#!/usr/bin/env python3 """promptc — compile shot spec + look into a ready-to-paste Flow/Veo prompt. tools/promptc.py shots/t2_s3_sharkrun.json # canonical look (cel80s) tools/promptc.py shots/t2_s3_sharkrun.json --look scannerdarkly tools/promptc.py shots/t2_s3_sharkrun.json --all # every look, labeled tools/promptc.py --flux hero --look scannerdarkly # ingredient still prompt # (hero_face for that look) Paths resolve relative to production/prompts/. Zero deps, stdlib only. """ import argparse import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent / "production" / "prompts" CANONICAL = "cel80s" def load(p): return json.loads(Path(p).read_text()) def looks(): return {f.stem: load(f) for f in (ROOT / "looks").glob("*.json")} def compile_prompt(shot, look, blocks): parts = [look["style"]] parts += [blocks["characters"][c] for c in shot.get("characters", [])] if shot.get("set"): parts.append(blocks["sets"][shot["set"]]) parts.append(shot["action"]) if shot.get("camera"): parts.append(shot["camera"]) neg = ", ".join(x for x in [blocks["negative_base"], look.get("negative", "")] if x) return ". ".join(p.strip().rstrip(".") for p in parts) + ". Avoid: " + neg + "." def flux_ingredient(char, look, blocks): """Prompt for the character ingredient still in a given look (FLUX, free).""" return (f"{look['style']}. {blocks['characters'][char]}. close-up head and " f"shoulders portrait, three-quarter view, confident smirk, plain warm " f"gray background. Avoid: {blocks['negative_base']}, {look.get('negative','')}.") def main(): ap = argparse.ArgumentParser() ap.add_argument("shot", nargs="?", help="path to shot json (rel to production/prompts/)") ap.add_argument("--look", default=CANONICAL) ap.add_argument("--all", action="store_true", help="compile shot in every look") ap.add_argument("--flux", metavar="CHAR", help="emit FLUX ingredient-still prompt for a character") args = ap.parse_args() blocks = load(ROOT / "blocks.json") lks = looks() if args.look not in lks: sys.exit(f"unknown look {args.look!r}; have: {', '.join(sorted(lks))}") if args.flux: print(flux_ingredient(args.flux, lks[args.look], blocks)) return if not args.shot: ap.error("need a shot json (or --flux CHAR)") p = Path(args.shot) shot = load(p if p.exists() else ROOT / args.shot) chosen = sorted(lks) if args.all else [args.look] for name in chosen: if args.all: print(f"\n=== {name} — {lks[name]['label']} ===") print(compile_prompt(shot, lks[name], blocks)) if __name__ == "__main__": main()