reel-studio-2 / cli.py
xvepkj's picture
initial commit
3d2098f
Raw
History Blame
6.97 kB
#!/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()