#!/usr/bin/env python3
"""#104(B-2d) 원인 대조 — 선언한 스케일과 **그 샷이 요구하는 내용**이 양립하나.

★Codex 지적(2026-08-28) 수용: 「문안을 더 세게 쌓는 일이 아니다.」
 눈가림 판정에서 좁은 선언만 넓어졌는데(넓어짐 8 · 일치 4 · 좁아짐 0),
 다음은 생성기를 나무라기 전에 **상류가 그 샷에 무엇을 요구했는지**를 본다.

무료다 — 이번 주행 `records.json` 의 실발송 프롬프트만 읽는다.

## 무엇을 세나

- 선언 `- FRAMING SCALE:`
- `- KEY BACKGROUND ELEMENTS:` 에 실린 **요소 수** (`;` 로 갈린다)
- `- FRAME LAYOUT:` 에 실린 **배치 항목 수** (`;` 로 갈린다)
- 인물 수 (`- FIGURES:` 계열)
- 그리고 **bgfirst 경로인가** — 그 경로는 배경판이 먼저 나오고 최종 롤이
  「keep it EXACTLY: its camera, perspective」라 프레이밍이 **두 번** 정해진다

## 안 하는 것

수를 세어 「그래서 넓어졌다」고 단정하지 않는다. 표본이 6개다.
가리키는 방향만 적고, 반대 사례(있으면)를 같이 적는다.

usage:  framing_demand_contrast.py <episode_id>
"""
from __future__ import annotations

import json
import pathlib
import re
import sys

ROOT = pathlib.Path(__file__).resolve().parents[3]

# 넓기 순서 — 판정 결과와 맞대려면 순서가 있어야 한다
SCALES = ["extreme close-up", "close-up", "medium close-up", "medium shot",
          "medium wide shot", "wide shot", "extreme wide shot"]
_DECL = [("extreme close-up", "extreme close-up"), ("insert", "close-up"),
         ("close-up", "close-up"), ("medium close", "medium close-up"),
         ("medium wide", "medium wide shot"), ("medium", "medium shot"),
         ("extreme wide", "extreme wide shot"), ("wide", "wide shot")]


def norm(decl: str) -> str:
    low = decl.lower()
    for needle, scale in _DECL:
        if needle in low:
            return scale
    return "?"


def field(prompt: str, name: str) -> str:
    m = re.search(rf"^- {re.escape(name)}:\s*(.+)$", prompt, re.M)
    return m.group(1).strip() if m else ""


def count_items(value: str) -> int:
    """`;` 로 갈린 항목 수. 빈 값은 0."""
    return len([p for p in value.split(";") if p.strip()]) if value else 0


def main() -> int:
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    if not args:
        print(__doc__)
        return 2
    epi = args[0]

    recs = list(ROOT.glob(f"projects/*/images/{epi}/scene/recipe/records.json"))
    if not recs:
        print("★records.json 을 못 찾았다 — 결론 내지 마라.")
        return 1
    rec = json.loads(recs[0].read_text(encoding="utf-8"))

    rows = []
    for tag in sorted(k for k in rec if "::" not in k
                      and isinstance(rec[k], dict)
                      and isinstance(rec[k].get("roll_prompts"), dict)):
        v = rec[tag]
        roll = next(iter(v["roll_prompts"].values()))
        scale_raw = field(roll, "FRAMING SCALE")
        bgfirst_key = f"{tag}::bgfirst_bg"
        bg = rec.get(bgfirst_key) or {}
        bg_prompt = bg.get("effective_prompt") or bg.get("prompt") or ""
        rows.append({
            "shot": tag,
            "declared": norm(scale_raw),
            "declared_raw": scale_raw,
            "bg_elements": count_items(field(roll, "KEY BACKGROUND ELEMENTS")),
            "layout_items": count_items(field(roll, "FRAME LAYOUT")),
            "figures": field(roll, "FIGURES"),
            "path": "bgfirst" if bg_prompt else "직접",
            "bg_scale": norm(field(bg_prompt, "FRAMING SCALE"))
            if bg_prompt else "",
        })

    if not rows:
        print("★roll_prompts 를 하나도 못 읽었다 — 파싱이 틀렸다.")
        return 1

    # 눈가림 판정 결과가 있으면 나란히 붙인다 (없으면 빈칸)
    vpath = ROOT / "artifact" / "20260828_framing_scale" / "verdicts.json"
    seen = {}
    if vpath.exists():
        for r in json.loads(vpath.read_text(encoding="utf-8")):
            got = {}
            for alias, v in (r.get("by_model") or {}).items():
                if v.get("ok"):
                    got[alias] = (v.get("payload") or {}).get("scale")
            seen[r["shot"]] = got

    order = {s: i for i, s in enumerate(SCALES)}
    print(f"{'샷':9} {'선언':17} {'배경요소':>5} {'배치항목':>5} "
          f"{'경로':8} {'판정 (넓어진 칸수)'}")
    print("─" * 92)
    for r in rows:
        di = order.get(r["declared"])
        marks = []
        for alias, got in (seen.get(r["shot"]) or {}).items():
            gi = order.get(got or "")
            if di is None or gi is None:
                marks.append(f"{alias}=?")
            else:
                delta = gi - di
                marks.append(f"{alias}={'+' if delta > 0 else ''}{delta}")
        print(f"{r['shot']:9} {r['declared']:17} {r['bg_elements']:>5} "
              f"{r['layout_items']:>5} {r['path']:8} {' · '.join(marks)}")

    print("\n── bgfirst 경로는 프레이밍이 두 번 정해진다 ──")
    for r in rows:
        if r["path"] == "bgfirst":
            same = "같음" if r["bg_scale"] == r["declared"] else "★다름"
            print(f"  {r['shot']}: 배경판 선언={r['bg_scale'] or '(없음)'} · "
                  f"최종 선언={r['declared']} → {same}")

    print("\n★수를 세어 인과를 단정하지 않는다 — 표본 6이다. "
          "반대 사례가 있으면 위 표에 그대로 보인다.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
