recordstoreguy/tools/promptc.py
m3ultra c4ec145455 Style-swap prompt system: blocks.json + 4 looks + promptc compiler
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:45:30 +10:00

78 lines
2.8 KiB
Python
Executable File

#!/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()