Compare commits
22 Commits
feat
...
78e35d65fd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78e35d65fd | ||
| d737177eab | |||
| 6420a02090 | |||
| 2be19ee02c | |||
| 98613a0002 | |||
| 501c45cad7 | |||
| 0fd0cda460 | |||
| dd4f9fc51c | |||
| 6288d77d46 | |||
|
|
8f5934d576 | ||
|
|
a941eb6b98 | ||
|
|
503f2817d2 | ||
|
|
85b5717595 | ||
|
|
9c626a1e4a | ||
| ad84469037 | |||
| 561be6a182 | |||
|
|
1e15544687 | ||
|
|
927eabb2d5 | ||
|
|
1425f852e6 | ||
|
|
95d287bafc | ||
|
|
5bb58c98e5 | ||
|
|
55c7ccf316 |
164
components/video.py
Normal file
164
components/video.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import os
|
||||
|
||||
from moviepy.video.io.VideoFileClip import VideoFileClip
|
||||
from moviepy.video.VideoClip import ColorClip
|
||||
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
|
||||
from moviepy import TextClip
|
||||
|
||||
font = "./Montserrat.ttf"
|
||||
font_size = 70
|
||||
video_codec = "libx264"
|
||||
|
||||
def auto_wrap_text(text, max_width):
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
words = text.split()
|
||||
lines = []
|
||||
|
||||
line = ""
|
||||
for word in words:
|
||||
test_line = f"{line} {word}".strip()
|
||||
test_clip = TextClip(text=test_line, font=font, font_size=font_size, color='white', method='label')
|
||||
|
||||
if test_clip.w > max_width and line != "":
|
||||
lines.append(line)
|
||||
line = word
|
||||
else:
|
||||
line = test_line
|
||||
|
||||
test_clip.close()
|
||||
|
||||
lines.append(line)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def cut_video_new_clip(input_path: str, start: float, end: float, output_path: str):
|
||||
|
||||
|
||||
with VideoFileClip(input_path) as clip:
|
||||
segment = clip.subclipped(start, end)
|
||||
fps = clip.fps or 30
|
||||
|
||||
segment.write_videofile(
|
||||
output_path,
|
||||
codec=video_codec,
|
||||
remove_temp=True,
|
||||
fps=fps,
|
||||
bitrate="3000k",
|
||||
ffmpeg_params=[
|
||||
"-preset", "ultrafast",
|
||||
"-tune", "zerolatency",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.1"
|
||||
]
|
||||
)
|
||||
|
||||
def process_segment(input_path: str, top_text: str = "", bottom_text: str = "", filename="", idx=1) -> str:
|
||||
os.makedirs("outputs", exist_ok=True)
|
||||
os.makedirs(f"outputs/{filename}", exist_ok=True)
|
||||
|
||||
final_width, final_height = 1080, 1920
|
||||
top_h, middle_h, bottom_h = 480, 960, 480
|
||||
|
||||
with VideoFileClip(input_path) as clip:
|
||||
dur = clip.duration
|
||||
bg = ColorClip(size=(final_width, final_height), color=(0, 0, 0), duration=dur)
|
||||
video_resized = clip.resized(width=final_width)
|
||||
y = top_h + (middle_h - video_resized.h) // 2
|
||||
video_resized = video_resized.with_position((0, y))
|
||||
wrapped_top_text = auto_wrap_text(top_text, final_width - 40)
|
||||
wrapped_bottom_text = auto_wrap_text(bottom_text, final_width - 40)
|
||||
|
||||
txt_top = TextClip(
|
||||
text=wrapped_top_text,
|
||||
font_size=70,
|
||||
color="white",
|
||||
font=font,
|
||||
method="label",
|
||||
size=(final_width, top_h)
|
||||
).with_duration(dur).with_position((0, 0))
|
||||
|
||||
txt_bot = TextClip(
|
||||
text=wrapped_bottom_text,
|
||||
font_size=70,
|
||||
color="white",
|
||||
font=font,
|
||||
method="label",
|
||||
size=(final_width, bottom_h),
|
||||
).with_duration(dur).with_position((0, final_height - bottom_h))
|
||||
|
||||
final = CompositeVideoClip([bg, video_resized, txt_top, txt_bot], size=(final_width, final_height))
|
||||
|
||||
output_path = f"outputs/{filename}/clip_{idx}.mp4"
|
||||
|
||||
final.write_videofile(
|
||||
output_path,
|
||||
codec=video_codec,
|
||||
remove_temp=True,
|
||||
fps=30,
|
||||
bitrate="3000k",
|
||||
ffmpeg_params=[
|
||||
"-preset", "ultrafast",
|
||||
"-tune", "zerolatency",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.1"
|
||||
]
|
||||
)
|
||||
|
||||
final.close()
|
||||
|
||||
return output_path
|
||||
|
||||
def timestamp_to_seconds(ts):
|
||||
if isinstance(ts, (int, float)):
|
||||
return ts
|
||||
|
||||
parts = ts.split(":")
|
||||
parts = [float(p) for p in parts]
|
||||
|
||||
if len(parts) == 3:
|
||||
h, m, s = parts
|
||||
return h * 3600 + m * 60 + s
|
||||
elif len(parts) == 2:
|
||||
m, s = parts
|
||||
return m * 60 + s
|
||||
elif len(parts) == 1:
|
||||
return parts[0]
|
||||
else:
|
||||
raise ValueError(f"Timestamp inválido: {ts}")
|
||||
|
||||
def process_full_video(filename: str, times: list = None) -> list:
|
||||
os.makedirs("temp", exist_ok=True)
|
||||
|
||||
times = times or []
|
||||
video_path = f"videos/{filename}"
|
||||
processed = []
|
||||
|
||||
print(f"Total de trechos: {len(times)}")
|
||||
print(f"Codec de render: {video_codec}")
|
||||
|
||||
for idx, interval in enumerate(times, start=1):
|
||||
start = timestamp_to_seconds(interval.get("start", 0))
|
||||
end_raw = interval.get("end", None)
|
||||
end = timestamp_to_seconds(end_raw) if end_raw is not None else None
|
||||
top_text = interval.get("topText", "")
|
||||
bottom_text = interval.get("bottomText", "")
|
||||
|
||||
if end is None:
|
||||
with VideoFileClip(video_path) as clip:
|
||||
end = clip.duration
|
||||
|
||||
print(f"Cortando trecho {idx}: {start}s a {end}s")
|
||||
|
||||
temp_path = f"temp/{os.path.splitext(filename)[0]}_{idx}.mp4"
|
||||
|
||||
cut_video_new_clip(video_path, start, end, temp_path)
|
||||
|
||||
out = process_segment(temp_path, top_text, bottom_text, filename, idx)
|
||||
|
||||
processed.append(out)
|
||||
|
||||
return processed
|
||||
@@ -40,4 +40,4 @@ RUN mkdir -p /app/videos /app/outputs
|
||||
|
||||
VOLUME ["/app/videos", "/app/outputs"]
|
||||
|
||||
CMD ["python", "-u", "main.py"]
|
||||
CMD ["python", "-u", "main.py"]
|
||||
|
||||
Reference in New Issue
Block a user