#!/usr/bin/env python3
"""#104(B-2d) — 선언한 FRAMING SCALE 을 완성 스틸이 지켰나. **눈가림.**

★canary 는 close-up 선언 8/8 위반을 봤다. 그건 PR #31·#32·#39 **병합 전**
 프롬프트로 만든 그림이었다. 이 도구는 **병합 뒤 완주한 주행의 최종 스틸**로
 같은 것을 다시 잰다.

## 재는 법 — 08-28 Codex BLOCK 을 되풀이하지 않는다

지난 판정은 **계약 줄을 먼저 보여 주고 낱말까지 골라 준 뒤** 물어서
「98%」 같은 수가 나왔다. 그건 그림을 본 것과 계약을 되풀이한 것을 못 가른다.

그래서 여기서는:

- 판정자에게 **계약을 안 보여 준다.** 사진 한 장만 준다
- 「close-up 인가?」라고 묻지 않는다 — **표준 스케일 일곱 중 하나를 고르게**
  한다. 어느 쪽이 정답인지 알 길이 없다
- 선언값과의 대조는 **판정 뒤 코드가** 한다
- 모델은 지시대로 **Gemini 3.1 Pro + 최신 Grok 둘**. 합치지 않고 각각 남긴다

usage:  judge_framing_scale.py <episode_id> [--dry]
"""
from __future__ import annotations

import json
import pathlib
import re
import sys

sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[2]))
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"]

SYSTEM = (
    "You are a film-still framing checker. You are shown ONE photograph and "
    "nothing else. Report only what is visible. Do not speculate about "
    "intent, story, or how the image was made.")

SCHEMA = {
    "type": "object",
    "properties": {
        "scale": {"type": "string", "enum": SCALES},
        "subject_en": {"type": "string"},
        "why_en": {"type": "string"},
    },
    "required": ["scale", "subject_en", "why_en"],
    "additionalProperties": False,
}

# 선언값 → 표준 스케일 (선언은 자유 문장이라 앞부분으로 가른다)
_DECL = [
    ("extreme close-up", "extreme close-up"),
    ("insert", "close-up"),          # insert close-up on a detail
    ("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"),
]
# 넓기 순서 — 「넓어졌나」를 재려면 순서가 있어야 한다
ORDER = {s: i for i, s in enumerate(SCALES)}


def normalize(decl: str) -> str | None:
    low = decl.lower()
    for needle, scale in _DECL:
        if needle in low:
            return scale
    return None


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

    from sqlalchemy import create_engine, text
    from app.core.config import settings

    # ★선언과 이미지는 **같은 `image_asset` 행**에서 읽는다 (Codex BLOCK 수용,
    #  2026-08-28). 종전 판은 `records.json` 의 태그별 선언과 DB 의 현재
    #  primary 이미지를 **따로** 읽어 `Sxshy` 로 붙였다. force/rerun 뒤에는
    #  프롬프트 세대와 이미지 세대가 갈릴 수 있고 `is_primary` 만으로는 그
    #  짝 불일치를 **못 막는다** — 그러면 옛 프롬프트로 새 그림을 재게 된다.
    #  `a.prompt_used` 는 그 이미지를 만든 **바로 그 프롬프트**다.
    eng = create_engine(settings.database_url)
    with eng.connect() as c:
        rows = c.execute(text(
            "SELECT s.scene_index, s.shot_index, a.file_path, a.prompt_used "
            "FROM scene_still s JOIN image_asset a ON a.still_id=s.id "
            f"WHERE s.episode_id='{epi}' AND a.asset_type='scene' "
            "AND a.is_primary=1 "
            "AND a.prompt_used IS NOT NULL AND a.prompt_used <> '' "
            "AND (a.is_intermediate IS NULL OR a.is_intermediate = false) "
            "AND a.source_image_id IS NULL AND a.parent_image_id IS NULL "
            "ORDER BY s.scene_index, s.shot_index")).fetchall()

    jobs = []
    for si, shi, path, prompt in rows:
        tag = f"S{si}sh{shi}"
        m = re.search(r"^- FRAMING SCALE: (.+)$", prompt or "", re.M)
        if not m:
            print(f"  {tag}: 이 자산의 프롬프트에 FRAMING SCALE 선언이 없다 "
                  f"— 건너뛴다(없다고 읽지 않는다)")
            continue
        raw = m.group(1).strip()
        p = ROOT / path
        if not p.exists():
            p = pathlib.Path(path)
        if not p.exists():
            print(f"  {tag}: 파일 없음 {path}")
            continue
        jobs.append((tag, raw, normalize(raw), p))

    print(f"판정 대상 {len(jobs)}장 × 모델 2 = {len(jobs)*2} 호출")
    for tag, raw, norm, p in jobs:
        print(f"  {tag}: 선언 {raw!r} → {norm}")
    if dry:
        print("\n--dry 라 호출 0")
        return 0

    from app.modules.llm.dual_vlm import ask_both
    from app.modules.pipeline.multiroll_gemini import png_part

    out = []
    for i, (tag, raw, norm, p) in enumerate(jobs, 1):
        head = ("Look at this photograph and classify its framing scale.\n"
                "Choose exactly one from: " + ", ".join(SCALES) + ".\n"
                "- scale: the framing scale you see\n"
                "- subject_en: what the frame is actually of\n"
                "- why_en: one short sentence on how much of the subject "
                "fills the frame")
        parts = [{"type": "text", "text": head},
                 {"type": "text", "text": "PHOTOGRAPH:"},
                 png_part(p.read_bytes())]
        dual = ask_both("framing_scale", SYSTEM, parts, SCHEMA,
                        schema_name="framing_scale")
        row = {"shot": tag, "declared_raw": raw, "declared": norm,
               "by_model": {}}
        for call in dual.calls:
            row["by_model"][call.alias] = {
                "ok": call.ok, "payload": call.payload,
                "error": getattr(call, "error", None)}
        out.append(row)
        seen = {a: (v["payload"] or {}).get("scale")
                for a, v in row["by_model"].items() if v["ok"]}
        print(f"  [{i}/{len(jobs)}] {tag} 선언={norm} 판정={seen}")

    d = ROOT / "artifact" / "20260828_framing_scale"
    d.mkdir(parents=True, exist_ok=True)
    (d / "verdicts.json").write_text(
        json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")

    print("\n═══ 대조 (합치지 않는다) ═══")
    for r in out:
        di = ORDER.get(r["declared"] or "", None)
        for alias, v in r["by_model"].items():
            if not v["ok"]:
                print(f"  {r['shot']:8} {alias:11} ✗ {v['error']}")
                continue
            got = (v["payload"] or {}).get("scale")
            gi = ORDER.get(got, None)
            if di is None or gi is None:
                mark = "?"
            elif gi == di:
                mark = "일치"
            elif gi > di:
                mark = f"★넓어짐 +{gi-di}"
            else:
                mark = f"좁아짐 -{di-gi}"
            print(f"  {r['shot']:8} {alias:11} 선언={r['declared']:17}"
                  f" 판정={got:17} {mark}")
    print(f"\n적었다: {(d/'verdicts.json').relative_to(ROOT)}")
    return 0


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