Video-Gen / app /core /compiler.py
jacky3102's picture
Upload 33 files
9459cd0 verified
Raw
History Blame Contribute Delete
3.03 kB
"""
Compiles a Timeline into an ffmpeg filter_complex graph + input args.
This is the one place that knows what "zoom_in" or "fade" means in ffmpeg
filter syntax. Everything upstream (API, renderer) only ever talks about the
Timeline's abstract Scene/Layer/Clip model.
"""
from app.core.timeline import Timeline, Clip
OUT_W, OUT_H, FPS = 1920, 1080, 30
def _image_animation_filter(clip: Clip, idx: int) -> str:
"""Ken Burns-style zoompan for a still image input."""
frames = int(clip.duration * FPS)
zp = "zoompan=z='min(zoom+0.0015,1.5)':d={frames}:s={w}x{h}:fps={fps}"
pan_left = "zoompan=z='1.2':x='iw*0.2-on/({frames}/(iw*0.2))':d={frames}:s={w}x{h}:fps={fps}"
pan_right = "zoompan=z='1.2':x='on*(iw*0.2)/{frames}':d={frames}:s={w}x{h}:fps={fps}"
animations = {
"zoom_in": zp,
"zoom_out": "zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':d={frames}:s={w}x{h}:fps={fps}",
"pan_left": pan_left,
"pan_right": pan_right,
"fade": zp, # base motion + fade handled separately via transition filter
"blur": zp,
"rotate": zp,
"parallax": zp,
"none": "scale={w}:{h}",
}
template = animations.get(clip.animation, animations["none"])
return template.format(frames=frames, w=OUT_W, h=OUT_H, fps=FPS)
def _transition_filter(clip: Clip, stream_label: str) -> str:
"""Wraps a stream with its entry transition (fade-in style transitions
for v1; xfade-based cross-clip transitions are the natural v2 extension
once multiple concurrent streams are common)."""
d = clip.transition_duration
if clip.transition_in in ("fade", "cross"):
return f"[{stream_label}]fade=t=in:st=0:d={d}[{stream_label}tr]"
return f"[{stream_label}]null[{stream_label}tr]"
def compile_filter_graph(timeline: Timeline) -> dict:
"""
Returns {"inputs": [...ffmpeg -i args...], "filter_complex": str,
"video_out_label": str, "map_args": [...]}
"""
input_args = []
filter_lines = []
concat_labels = []
for idx, clip in enumerate(timeline.clips):
stream = f"v{idx}"
if clip.kind == "image":
input_args += ["-loop", "1", "-t", str(clip.duration), "-i", clip.local_path]
vf = _image_animation_filter(clip, idx)
filter_lines.append(f"[{idx}:v]{vf},setsar=1[{stream}]")
else: # video
input_args += ["-i", clip.local_path]
filter_lines.append(
f"[{idx}:v]scale={OUT_W}:{OUT_H}:force_original_aspect_ratio=decrease,"
f"pad={OUT_W}:{OUT_H}:(ow-iw)/2:(oh-ih)/2,setsar=1,trim=duration={clip.duration}[{stream}]"
)
filter_lines.append(_transition_filter(clip, stream))
concat_labels.append(f"[{stream}tr]")
n = len(timeline.clips)
filter_lines.append(f"{''.join(concat_labels)}concat=n={n}:v=1:a=0[vout]")
return {
"input_args": input_args,
"filter_complex": ";".join(filter_lines),
"video_out_label": "vout",
}