Ghostlight/scripts/count_visual_images.py

109 lines
4.3 KiB
Python
Raw Normal View History

2026-05-03 11:22:57 +01:00
"""Count base images and image variants declared by Ghostlight visual plans."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_VISUAL_GLOB = "examples/visual/*.visual.json"
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def plural(count: int, singular: str, plural_form: str | None = None) -> str:
return f"{count} {singular if count == 1 else (plural_form or singular + 's')}"
def count_visual_plan(path: Path) -> dict[str, Any]:
data = load_json(path)
if "visual_art_direction" in data:
raise ValueError(f"{path} uses legacy visual_art_direction; count visual_scene_plan instead")
2026-05-03 11:22:57 +01:00
scene_plan = data.get("visual_scene_plan", {})
scenes = scene_plan.get("scenes", [])
global_branch_modifiers = scene_plan.get("global_branch_image_modifiers", [])
scene_variant_counts = []
for scene in scenes:
modifiers = scene.get("state_image_modifiers", [])
scene_variant_counts.append(
{
"visual_scene_id": scene.get("visual_scene_id", "<missing>"),
"ink_anchor": scene.get("ink_anchor", "<missing>"),
"base_images": 1,
"scene_variants": len(modifiers),
}
)
base_images = len(scenes)
scene_variants = sum(item["scene_variants"] for item in scene_variant_counts)
global_variants = len(global_branch_modifiers)
return {
"path": path,
"visual_plan_id": data.get("visual_plan_id", path.stem),
"base_images": base_images,
"scene_variants": scene_variants,
"global_branch_variants": global_variants,
"total_variants": scene_variants + global_variants,
"max_exhaustive_renders": base_images + scene_variants + global_variants,
2026-05-03 11:22:57 +01:00
"scene_variant_counts": scene_variant_counts,
}
def format_report(results: list[dict[str, Any]], verbose: bool) -> str:
lines: list[str] = []
total_base = sum(item["base_images"] for item in results)
total_scene_variants = sum(item["scene_variants"] for item in results)
total_global_variants = sum(item["global_branch_variants"] for item in results)
total_variants = total_scene_variants + total_global_variants
2026-05-03 11:22:57 +01:00
lines.append("Ghostlight visual image inventory")
lines.append("")
for item in results:
lines.append(f"- {item['path'].as_posix()}")
lines.append(f" visual_plan_id: {item['visual_plan_id']}")
lines.append(f" base images: {item['base_images']}")
lines.append(f" variants: {item['total_variants']}")
lines.append(f" scene-local variants: {item['scene_variants']}")
lines.append(f" global branch variants: {item['global_branch_variants']}")
lines.append(f" max exhaustive renders: {item['max_exhaustive_renders']}")
if verbose:
lines.append(" scene breakdown:")
for scene in item["scene_variant_counts"]:
lines.append(
f" - {scene['visual_scene_id']} ({scene['ink_anchor']}): "
f"{plural(scene['scene_variants'], 'variant')}"
)
lines.append("")
lines.append("Totals")
lines.append(f"- base images: {total_base}")
lines.append(f"- variants: {total_variants}")
lines.append(f" scene-local variants: {total_scene_variants}")
lines.append(f" global branch variants: {total_global_variants}")
lines.append(f"- max exhaustive renders: {total_base + total_variants}")
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="*", type=Path, help="Visual plan JSON files to count.")
parser.add_argument("--verbose", "-v", action="store_true", help="Print per-scene variant counts.")
args = parser.parse_args()
paths = args.paths or sorted(ROOT.glob(DEFAULT_VISUAL_GLOB))
if not paths:
raise SystemExit(f"No visual plans found with {DEFAULT_VISUAL_GLOB}")
results = [count_visual_plan(path if path.is_absolute() else ROOT / path) for path in paths]
print(format_report(results, verbose=args.verbose))
return 0
if __name__ == "__main__":
raise SystemExit(main())