Spaces:
Paused
Paused
| #!/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 <project_id> | |
| python cli.py topics <project_id> -n 8 | |
| python cli.py script <project_id> --topic "Why Venus spins backwards" | |
| python cli.py voice <project_id> --voice en-US-GuyNeural | |
| python cli.py media <project_id> | |
| python cli.py render <project_id> | |
| python cli.py clone <project_id> --url https://www.youtube.com/shorts/XXXX | |
| python cli.py run-all <project_id> [--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() | |