#!/usr/bin/env python3 """Reel Studio v2 CLI — run the whole pipeline headless (the debug path). Examples: python cli.py init --name "Space Facts" --niche "astronomy facts" \ --keywords "space,nasa,astronomy" --subreddits "space,astronomy" \ --refs "@besmartchannel,@AstroKobi" python cli.py style python cli.py topics -n 8 python cli.py script --topic "Why Venus spins backwards" python cli.py voice --voice en-US-GuyNeural python cli.py media python cli.py render python cli.py clone --url https://www.youtube.com/shorts/XXXX python cli.py run-all [--topic "..."] python cli.py list """ from __future__ import annotations import argparse import json import sys from core.contracts import NicheProfile, ReferenceEntry, to_dict from core.pipeline import Pipeline from core.utils import log def _progress(msg: str) -> None: log.info(">> %s", msg) def _load_project(pipe: Pipeline, project_id: str): project = pipe.store.load_project(project_id) if not project: sys.exit(f"No project {project_id!r}. Run `python cli.py list`.") return project def _print(obj) -> None: print(json.dumps(to_dict(obj), indent=2, ensure_ascii=False)) def main() -> None: ap = argparse.ArgumentParser(description="Reel Studio v2 — headless pipeline") sub = ap.add_subparsers(dest="cmd", required=True) p = sub.add_parser("init", help="create a project") p.add_argument("--name", required=True) p.add_argument("--niche", required=True, help="niche topic, e.g. 'astronomy facts'") p.add_argument("--keywords", default="", help="comma separated") p.add_argument("--subreddits", default="", help="comma separated") p.add_argument("--audience", default="") p.add_argument("--tone", default="") p.add_argument("--format", default="youtube_short", choices=["youtube_short", "instagram_reel"]) p.add_argument("--refs", default="", help="comma separated YouTube @handles / URLs / names") p = sub.add_parser("suggest-refs", help="AI-suggest reference channels") p.add_argument("project_id") p.add_argument("-n", type=int, default=5) p = sub.add_parser("style", help="build/refresh the style profile") p.add_argument("project_id") p = sub.add_parser("topics", help="generate ranked topics") p.add_argument("project_id") p.add_argument("-n", type=int, default=8) p = sub.add_parser("reels", help="list reels in a project") p.add_argument("project_id") p = sub.add_parser("script", help="generate the script (creates a new reel unless --reel)") p.add_argument("project_id") p.add_argument("--topic", required=True) p.add_argument("--reel", default=None, help="reel id (default: create a new reel)") p = sub.add_parser("voice", help="synthesize the voice track") p.add_argument("project_id") p.add_argument("--reel", default=None, help="reel id (default: latest reel)") p.add_argument("--voice", default="") p.add_argument("--provider", default=None, help="tts provider: gemini_tts|elevenlabs|edge_tts") p = sub.add_parser("media", help="gather clips/sfx/music") p.add_argument("project_id") p.add_argument("--reel", default=None, help="reel id (default: latest reel)") p = sub.add_parser("render", help="render 2-3 variants") p.add_argument("project_id") p.add_argument("--reel", default=None, help="reel id (default: latest reel)") p = sub.add_parser("clone", help="clone a reel from a YouTube Shorts URL (creates a new reel)") p.add_argument("project_id") p.add_argument("--url", required=True) p = sub.add_parser("run-all", help="style->topics->script->voice->media->render") p.add_argument("project_id") p.add_argument("--topic", default=None) p.add_argument("--voice", default="") sub.add_parser("list", help="list projects") args = ap.parse_args() pipe = Pipeline() if args.cmd == "init": refs = [ ReferenceEntry(kind="youtube", name=r.strip(), url=r.strip() if "/" in r else "") for r in args.refs.split(",") if r.strip() ] niche = NicheProfile( topic=args.niche, keywords=[k.strip() for k in args.keywords.split(",") if k.strip()], subreddits=[s.strip() for s in args.subreddits.split(",") if s.strip()], audience=args.audience, tone=args.tone, ) project = pipe.create_project(args.name, niche, args.format, refs) print(f"Created project {project.id}") _print(project) return if args.cmd == "list": for proj in pipe.store.list_projects(): print(f"{proj.id} {proj.name} [{proj.format}] niche={proj.niche.topic}") return project = _load_project(pipe, args.project_id) def resolve_reel(create_topic: str | None = None) -> str: """Use --reel, else the latest reel, else (if create_topic) a new one.""" if getattr(args, "reel", None): return args.reel reels = pipe.list_reels(project) if reels and create_topic is None: return reels[-1].id reel = pipe.create_reel(project, topic=create_topic or "") print(f"Created reel {reel.id}") return reel.id if args.cmd == "suggest-refs": _print(pipe.suggest_references(project, n=args.n)) elif args.cmd == "style": _print(pipe.build_style(project, progress=_progress)) elif args.cmd == "topics": _print(pipe.generate_topics(project, n=args.n, progress=_progress)) elif args.cmd == "reels": for r in pipe.list_reels(project): print(f"{r.id} {r.name} topic={r.topic!r}") elif args.cmd == "script": rid = resolve_reel(create_topic=args.topic) _print(pipe.generate_script(project, rid, args.topic)) elif args.cmd == "voice": rid = resolve_reel() _print(pipe.synthesize_voice(project, rid, voice=args.voice, provider=args.provider, progress=_progress)) elif args.cmd == "media": rid = resolve_reel() _print(pipe.gather_media(project, rid, _progress)) elif args.cmd == "render": rid = resolve_reel() batch = pipe.render(project, rid, progress=_progress) _print(batch) for r in batch.results: print(f"\n{r.variant}: {r.path}") elif args.cmd == "clone": result = pipe.clone_reel(project, args.url, _progress) print(f"Created reel {result['reel'].id}") _print(result["script"]) elif args.cmd == "run-all": batch = pipe.run_all(project, topic=args.topic, voice=args.voice, progress=_progress) for r in batch.results: print(f"{r.variant}: {r.path}") if __name__ == "__main__": main()