#!/usr/bin/env python3 """sync_prompts_catalog.py — freeze the art inputs into the repo. The metaphors and captions are authored in the Ideogram 4 tool dir (`C:/AI/ideogram4/`), which is NOT in this repository. Without this script the repository could not reproduce a single frame: `prompts_catalog.json` would still describe the 21 shared metaphors that produced the near-duplicate v1-adjacent art. This copies the authoring state into the repo so git alone is enough: scripts/og-gen/metaphors.json slug -> the unique metaphor (72/72 distinct) scripts/og-gen/captions/.json the exact structured caption sent to the model scripts/og-gen/prompts_catalog.json same shape as before, refreshed from the above scripts/og-gen/banner-plan.json the banner/thumb crop window per slug, computed by `C:/AI/ideogram4/plan_banner_crops.py` (detector in `probe_band_crop.py`). `render_from_art.mjs` reads this by default; without it a slug falls back to a weaker on-the-fly heuristic. Usage: python scripts/og-gen/sync_prompts_catalog.py # uses C:/AI/ideogram4 python scripts/og-gen/sync_prompts_catalog.py --tool-dir D:/other python scripts/og-gen/sync_prompts_catalog.py --check # report only, no writes """ from __future__ import annotations import argparse import importlib.util import json import shutil import sys from pathlib import Path REPO = Path(__file__).resolve().parents[2] OG = REPO / "scripts" / "og-gen" def load_metaphors(tool_dir: Path) -> dict[str, str]: src = tool_dir / "build_captions.py" if not src.exists(): raise SystemExit(f"no build_captions.py under {tool_dir} (pass --tool-dir)") spec = importlib.util.spec_from_file_location("build_captions", src) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return dict(mod.METAPHORS) def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--tool-dir", default="C:/AI/ideogram4") ap.add_argument("--check", action="store_true", help="report only, write nothing") args = ap.parse_args() tool = Path(args.tool_dir) metaphors = load_metaphors(tool) posts = {p.stem for p in (REPO / "src" / "content" / "posts").glob("*.md")} catalog = json.loads((OG / "prompts_catalog.json").read_text(encoding="utf-8")) meta = json.loads((OG / "art-meta.json").read_text(encoding="utf-8")) problems = [] if set(metaphors) != posts: problems.append(f"slug mismatch: missing={sorted(posts - set(metaphors))[:4]} extra={sorted(set(metaphors) - posts)[:4]}") if len(set(metaphors.values())) != len(metaphors): problems.append(f"only {len(set(metaphors.values()))} distinct metaphors for {len(metaphors)} posts") missing_captions = [s for s in metaphors if not (tool / "captions" / f"{s}.json").exists()] if missing_captions: problems.append(f"{len(missing_captions)} captions absent from {tool / 'captions'}") missing_evidence = [s for s in metaphors if not meta.get(s, {}).get("evidence")] if missing_evidence: problems.append(f"{len(missing_evidence)} posts have no evidence line in art-meta.json") plan_file = tool / "out" / "banner-plan.json" plan = json.loads(plan_file.read_text(encoding="utf-8")) if plan_file.exists() else {"frames": {}} plan_missing = sorted(set(metaphors) - set(plan.get("frames", {}))) if plan_missing: problems.append(f"{len(plan_missing)} slugs have no banner crop plan " f"(first: {plan_missing[:4]}) — run plan_banner_crops.py") print(f"posts {len(posts)} · metaphors {len(metaphors)} · distinct {len(set(metaphors.values()))} · catalog {len(catalog)}") print("problems:", problems or "none") if problems: sys.exit(1) if args.check: print("--check: nothing written") return (OG / "metaphors.json").write_text( json.dumps({k: metaphors[k] for k in sorted(metaphors)}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", ) dest = OG / "captions" dest.mkdir(exist_ok=True) for slug in metaphors: shutil.copyfile(tool / "captions" / f"{slug}.json", dest / f"{slug}.json") by_slug = {e["slug"]: e for e in catalog} out = [] for slug in sorted(metaphors): entry = by_slug.get(slug, {"slug": slug}) caption = json.loads((tool / "captions" / f"{slug}.json").read_text(encoding="utf-8")) elems = caption.get("compositional_details", {}).get("elements", []) entry["unique_elements"] = metaphors[slug] entry["prompt"] = " ".join(e.get("desc", "") for e in elems).strip() or metaphors[slug] entry["evidence_summary"] = meta.get(slug, {}).get("evidence", entry.get("evidence_summary", "")) entry["caption"] = f"captions/{slug}.json" out.append(entry) (OG / "prompts_catalog.json").write_text(json.dumps(out, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") (OG / "banner-plan.json").write_text(json.dumps(plan, indent=1, ensure_ascii=False) + "\n", encoding="utf-8") fr = [f["silhouette"] for f in plan.get("frames", {}).values()] if fr: print(f"banner plan: {len(fr)} frames, silhouette " f"{min(fr)*100:.0f}-{max(fr)*100:.0f}% of the banner width") print(f"wrote metaphors.json, captions/ ({len(metaphors)} files), prompts_catalog.json, banner-plan.json") if __name__ == "__main__": main()