Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from bundle
Build or revise a reusable FFmpeg timeline for short-form video editing. Use when the user wants an agent-editable API for trimming clips, cropping, fitting to
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/video_timeline_editor/presentation/cli.py
1from __future__ import annotations23import argparse4import json5import runpy6import sys7from pathlib import Path89from video_timeline_editor import captions, clip, contain, cover, crop_box, project, text_overlay, transform10from video_timeline_editor.application.timeline_service import TimelineService11from video_timeline_editor.domain.model import Clip, Project12from video_timeline_editor.infrastructure.transcripts import resolve_path131415def load_timeline(timeline_path: Path) -> tuple[Project, list[Clip]]:16helper_globals = {17"Path": Path,18"PROJECT_DIR": timeline_path.parent.resolve(),19"SKILL_DIR": Path(__file__).resolve().parents[2],20"project": project,21"clip": clip,22"contain": contain,23"cover": cover,24"crop_box": crop_box,25"transform": transform,26"text_overlay": text_overlay,27"captions": captions,28}29scope = runpy.run_path(str(timeline_path), init_globals=helper_globals)30project_cfg = scope.get("PROJECT")31timeline = scope.get("TIMELINE")32if not isinstance(project_cfg, Project):33raise ValueError("Timeline file must define PROJECT = project(...)")34if not isinstance(timeline, list) or not all(isinstance(item, Clip) for item in timeline):35raise ValueError("Timeline file must define TIMELINE = [clip(...), ...]")36return project_cfg, timeline373839def print_dry_run(manifest: dict) -> None:40print(f"Timeline: {manifest['project']['output']}")41print(f"Frame: {manifest['project']['width']}x{manifest['project']['height']} @ {manifest['project']['fps']}fps")42print()43for clip_info in manifest["clips"]:44words = clip_info["caption_words"]45aligned = " word-snap" if clip_info["word_aligned_trim"] else ""46caption_suffix = f" captions={words}" if words else ""47print(48f"{clip_info['id']:12s} {clip_info['duration']:6.2f}s "49f"{clip_info['label']} "50f"[src {clip_info['actual_src_in']:.2f} -> {clip_info['actual_src_out']:.2f}]{aligned}{caption_suffix}"51)52print()53print(f"Estimated total: {manifest['total_duration']:.2f}s")545556def main() -> int:57parser = argparse.ArgumentParser(description="Render a declarative FFmpeg timeline")58parser.add_argument("--timeline", required=True, help="Path to timeline.py file")59parser.add_argument("--dry", action="store_true", help="Print the computed edit plan without rendering")60parser.add_argument("--only", nargs="+", help="Render only the listed clip ids, in timeline order")61parser.add_argument("--manifest", help="Write computed manifest JSON to this path")62parser.add_argument("--open", action="store_true", help="Open the rendered output when done")63args = parser.parse_args()6465timeline_path = Path(args.timeline).resolve()66project_cfg, timeline = load_timeline(timeline_path)67if args.only:68selected = set(args.only)69timeline = [entry for entry in timeline if entry.id in selected]70if not timeline:71raise ValueError("No clips matched --only")7273service = TimelineService(project_cfg, timeline_path.parent.resolve())74manifest = service.analyze_timeline(timeline)7576if args.manifest:77manifest_path = resolve_path(args.manifest, timeline_path.parent)78manifest_path.parent.mkdir(parents=True, exist_ok=True)79manifest_path.write_text(json.dumps(manifest, indent=2) + "\n")8081if args.dry:82print_dry_run(manifest)83return 08485final_manifest = service.render_timeline(timeline, open_after=args.open)86if args.manifest:87manifest_path = resolve_path(args.manifest, timeline_path.parent)88manifest_path.write_text(json.dumps(final_manifest, indent=2) + "\n")8990print(f"Output: {final_manifest['output']}")91print(f"Duration: {final_manifest['total_duration']:.2f}s")92return 0939495if __name__ == "__main__":96try:97raise SystemExit(main())98except Exception as exc:99print(str(exc), file=sys.stderr)100raise SystemExit(1)101