#!/usr/bin/env python3
"""s40 — 배경(플레이트)부터 스틸 선정까지 전부 3롤+Gemini VLM (2026-07-13).

사용자 지시(s39 후속): "배경부터 샷 선택까지 전부 다시. 배경과 참조
이미지를 모두 넣고(단일 참조 아님) Gemini 가 VLM 으로 판단하게 하자."

- 모든 생성 단계 = nb2 3롤(A/B/C) → Gemini VLM 단독 판정(0-10, 동점=
  랭킹) → 선정본이 다음 단계의 참조.
- 실외 플레이트 8: master2(한국 생활감 마스터) 참조.
- 실내 플레이트 6: s37 FP+뿌리+직전 '선정본' 체이닝.
- 스틸 23: 참조 = 배경 플레이트(항상) + prev 선정본(v3 배경 동일성
  판정, 배경 전용 샷 제외) + 캐릭터·소품(인물 샷) — 모두 첨부.
- s39 계약 유지: bgonly(배경 전용 샷=플레이트만+NO PEOPLE), 캐릭터
  traits, PROP_ORIENTATION, 자세 정본, carried, EXPRESSION_REALISM.

사용: backend/.venv/bin/python s40_full_multiref.py --only
     <ext_plates|int_plates|stills|html>
"""
import argparse
import html as _html
import shutil
import sys
from pathlib import Path

HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
import forest_lib as F  # noqa: E402
import s35_fullrun as R  # noqa: E402
import s37_rooftop_rebuild as W  # noqa: E402
import s34_stillrun as S  # noqa: E402
import s38_lightconti_full as L8  # noqa: E402
import s39_threeroll_vlm as N9  # noqa: E402
import x_plate_chain as PC  # noqa: E402

OUT = W.OUT.parent / "s40"
OUTB = OUT / "plates"
OUTS = OUT / "stills"
PAGE = HERE / "s40_full_multiref.html"
PLAN = "s40_full_v1"
CANDS = ["A", "B", "C"]
MASTER = L8.OUT / "master2.png"


# ★선정본 결함 수정 (07-13 사용자: "가장 좋은 것의 잘못된 부분이 있으면
#   수정 프롬프트를 생성시켜 nb2 i2i 로 그 이미지만 가지고 수정, 비교")
CRITIQUE40_SYS = "\n".join([
    "You are given an image-generation prompt, the labelled REFERENCE",
    "image(s) that were attached to it, and ONE photograph generated",
    "from them (the current best pick of three).",
    "List what is WRONG with it — ONLY violations of the prompt or of",
    "its reference instructions: the shot's stated moment and action,",
    "who/what must or must NOT be in frame, camera, time of day,",
    "location consistency with the reference, pose/immobility and",
    "carried-state clauses, object orientation, anatomy, and every",
    "exclusion (no text, no invented people or objects).",
    "Base every finding only on what is visible; ignore taste and",
    "generic aesthetics. For each issue give: issue_ko (short Korean",
    "line) + fix_en (ONE imperative English edit that fixes exactly",
    "that while changing nothing else). Empty array if nothing wrong.",
])
FIX40_HEAD = "\n".join([
    "Edit this photograph. Apply ONLY the corrections listed below,",
    "each in place, changing nothing else:",
])
FIX40_TAIL = "\n".join([
    "PRESERVE EVERYTHING ELSE EXACTLY as in the original photograph —",
    "every person, pose, face, garment, object, set feature, lighting,",
    "camera and framing stay identical. No text, captions or",
    "watermarks anywhere.",
])


def _critfix(rec_map, key, tag, prompt, stem):
    """선정본 critique(Gemini) → 결함 있으면 i2i 수정(그 이미지 단독
    참조) → _sel 을 수정본으로 교체(체인 앵커). 원본 후보 파일은 보존."""
    rec = rec_map[key]
    sel_path = stem.parent / f"{stem.name}_sel.png"
    orig = stem.parent / f"{stem.name}_{rec['selected'].lower()}.png"
    parts = [{"type": "text", "text": "THE PROMPT:\n" + prompt}]
    for r in rec.get("refs", []):
        parts.append({"type": "text",
                      "text": f"REFERENCE — {r['label']}"})
        parts.append(F.png_data_url(r["path"]))
    parts.append({"type": "text", "text": "Photograph to examine:"})
    parts.append(F.png_data_url(orig))
    crit = F.llm(f"s40_crit_{tag}", CRITIQUE40_SYS, parts,
                 W.T.CRITIQUE_SCHEMA, model="gemini-pro")
    issues = crit["issues"]
    rec["critique"] = issues
    if not issues:
        shutil.copy(orig, sel_path)
        rec["fix_skipped"] = True
        print(f"[s40] {tag}: 결함 0 — 선정본 그대로")
    else:
        fix_prompt = "\n\n".join([
            FIX40_HEAD,
            "CORRECTIONS:\n" + "\n".join(
                f"- {i['fix_en']}" for i in issues),
            FIX40_TAIL])
        fixed = stem.parent / f"{stem.name}_fix.png"
        F.img_nb2(f"s40_fix_{tag}", fix_prompt,
                  [(W.T.FIX_LABEL, orig)], aspect_ratio="16:9",
                  out_path=fixed)
        shutil.copy(fixed, sel_path)
        rec["fix_skipped"] = False
        rec["fix_prompt"] = fix_prompt
        print(f"[s40] {tag}: 결함 {len(issues)}건 → i2i 수정본이 선정본")


def _judge_gemini(tag, sys_prompt, prompt, ref_parts, cand_paths):
    parts = [{"type": "text",
              "text": "THE PROMPT (all three candidates were generated"
                      " from this):\n" + prompt}] + ref_parts
    for lab, p in zip(CANDS, cand_paths):
        parts.append({"type": "text", "text": f"Candidate {lab}:"})
        parts.append(F.png_data_url(p))
    judge = {"gemini-pro": F.llm(f"s40_judge_{tag}", sys_prompt, parts,
                                 W.T.JUDGE_SCHEMA, model="gemini-pro")}
    totals, selected = N9._gemini_select(judge)
    return judge, totals, selected


def _roll3(tag, prompt, refs, out_stem):
    paths = []
    for lab in CANDS:
        p = out_stem.parent / f"{out_stem.name}_{lab.lower()}.png"
        F.img_nb2(f"s40_{tag}_{lab.lower()}", prompt, refs,
                  aspect_ratio="16:9", out_path=p)
        paths.append(p)
    return paths


def _select(rec_map, key, tag, sys_prompt, prompt, refs, out_stem):
    """3롤 → Gemini 판정 → 선정 복사(_sel). 레코드 저장은 호출부."""
    cands = _roll3(tag, prompt, refs, out_stem)
    ref_parts = []
    for rlab, rp in refs:
        ref_parts.append({"type": "text", "text": f"REFERENCE — {rlab}"})
        ref_parts.append(F.png_data_url(rp))
    judge, totals, selected = _judge_gemini(tag, sys_prompt, prompt,
                                            ref_parts, cands)
    sel_path = out_stem.parent / f"{out_stem.name}_sel.png"
    shutil.copy(out_stem.parent
                / f"{out_stem.name}_{selected.lower()}.png", sel_path)
    rec_map[key] = {
        "prompt": prompt, "totals": totals, "selected": selected,
        "verdicts": judge["gemini-pro"]["verdicts"],
        "ranking": judge["gemini-pro"]["ranking"],
        "refs": [{"label": lab_, "path": str(p_)} for lab_, p_ in refs],
    }
    print(f"[s40] {tag}: totals={totals} sel={selected}")
    _critfix(rec_map, key, tag, prompt, out_stem)
    return sel_path


def _resume(rec_map, key, tag, prompt, stem):
    """재개: 선정 완료+critique 미실행이면 critique/fix 만 소급."""
    if "critique" not in (rec_map.get(key) or {}):
        _critfix(rec_map, key, tag, prompt, stem)
        return True
    return False


def stage_ext_plates():
    assert MASTER.exists(), f"마스터 없음: {MASTER}"
    p37 = L8._plan37()
    plan = F.load_plan(PLAN) if (HERE / "plans" / f"{PLAN}.json").exists() \
        else {}
    rec = plan.setdefault("ext_plates", {})
    OUTB.mkdir(parents=True, exist_ok=True)
    for tag, info in p37["ext_bg"].items():
        stem = OUTB / f"bg_{tag}"
        if (OUTB / f"bg_{tag}_sel.png").exists():
            if _resume(rec, tag, f"extbg_{tag}",
                       info["bg_prompt_en"], stem):
                F.save_plan(PLAN, plan)
            else:
                print(f"[ext_plates] {tag} skip(exists)")
            continue
        _select(rec, tag, f"extbg_{tag}", PC.JUDGE_SYS,
                info["bg_prompt_en"],
                [(W.MASTER_REF_LABEL, MASTER)], stem)
        F.save_plan(PLAN, plan)


def stage_int_plates():
    p37 = L8._plan37()
    plan = F.load_plan(PLAN)
    rec = plan.setdefault("int_plates", {})
    OUTB.mkdir(parents=True, exist_ok=True)
    fp = W.OUT / W.FP_FILE
    root_path = None
    prev_path = None
    for i, p_ in enumerate(p37["int_plates"]):
        key = p_["key"]
        stem = OUTB / f"plate_{key}"
        sel = OUTB / f"plate_{key}_sel.png"
        if sel.exists():
            if _resume(rec, key, f"intpl_{key}", p_["prompt_en"], stem):
                F.save_plan(PLAN, plan)
            else:
                print(f"[int_plates] {key} skip(exists)")
        else:
            refs = [(W.FP_REF_LABEL, fp)]
            if i > 0:
                refs.append((W.ROOT_REF_LABEL, root_path))
                if prev_path != root_path:
                    refs.append((W.PREV_REF_LABEL, prev_path))
            _select(rec, key, f"intpl_{key}", N9.JUDGE_SYSTEM,
                    p_["prompt_en"], refs, stem)
            F.save_plan(PLAN, plan)
        if i == 0:
            root_path = sel
        prev_path = sel


def _plate40(tag, gkey, p37):
    if gkey != W.INT_GROUP:
        return OUTB / f"bg_{tag}_sel.png"
    for p_ in p37["int_plates"]:
        if tag in p_["covers"]:
            return OUTB / f"plate_{p_['key']}_sel.png"
    raise SystemExit(f"실내 플레이트 미배정: {tag}")


def stage_stills():
    p37 = L8._plan37()
    plan35 = W._s35()
    mv = W._movement(plan35)
    gmap = {g["key"]: g for g in plan35["groups"]}
    tag2g = {t: gk for gk in W.EXT_GROUPS for t in gmap[gk]["shots"]}
    tag2g.update({t: W.INT_GROUP for t in p37["int_tags"]})
    prev2 = F.load_plan(L8.PLAN)["prev2"]
    bgonly = F.load_plan(N9.PLAN)["bgonly"]
    chars = S._char_refs()
    props = S._prop_refs()
    ve = S._ve_ids()
    scene_union = {}
    for (si, _shi), ids in ve.items():
        scene_union.setdefault(si, set()).update(ids)
    plan = F.load_plan(PLAN)
    rec = plan.setdefault("stills", {})
    OUTS.mkdir(parents=True, exist_ok=True)
    for tag in L8._all_tags_story_order(p37):
        stem = OUTS / tag
        if (OUTS / f"{tag}_sel.png").exists():
            if rec.get(tag) and _resume(rec, tag, f"still_{tag}",
                                        rec[tag]["prompt"], stem):
                F.save_plan(PLAN, plan)
            else:
                print(f"[stills] {tag} skip(exists)")
            continue
        gkey = tag2g[tag]
        plate = _plate40(tag, gkey, p37)
        assert plate.exists(), f"플레이트 없음: {plate}"
        bg_only = not bgonly[tag]["person_visible"]
        pj = prev2.get(tag) or {}
        prev_tag = None if bg_only else pj.get("prev")
        prev_usage = pj.get("usage_en", "")
        prev_used = None
        # ★참조 다중 첨부: 플레이트(항상) + prev 선정본 + 엔티티
        refs = [(W.PLATE_STILL_LABEL, plate)]
        if prev_tag and (OUTS / f"{prev_tag}_sel.png").exists():
            prev_used = prev_tag
            refs.append((W.PREV_STILL_LABEL,
                         OUTS / f"{prev_tag}_sel.png"))
        if not bg_only:
            ve_ids = ve.get(R._key(tag), [])
            if not ve_ids:
                ve_ids = sorted(scene_union.get(R._key(tag)[0], set()))
            for cid in ve_ids:
                if cid in chars:
                    name, p = chars[cid]
                    if p.exists():
                        refs.append((
                            f"CHARACTER REFERENCE — {name}: the exact"
                            " person appearing in this shot; match face,"
                            " hair and build exactly.", p))
                elif cid in props:
                    name, p = props[cid]
                    if p.exists():
                        refs.append((
                            f"PROP REFERENCE — {name}: the exact object"
                            " appearing in this shot; match its look,"
                            " material and wear exactly.", p))
        prompt = N9._build_prompt(tag, gkey, p37, mv, gmap,
                                  prev_used, prev_usage, bg_only=bg_only)
        _select(rec, tag, f"still_{tag}", N9.JUDGE_SYSTEM, prompt,
                refs, stem)
        rec[tag].update({
            "gkey": gkey, "prev_used": prev_used, "bg_only": bg_only,
            "prev_usage": prev_usage if prev_used else "",
            "ref_mode": ("플레이트만 (배경 전용)" if bg_only
                         else "플레이트+prev+엔티티" if prev_used
                         else "플레이트+엔티티"),
        })
        F.save_plan(PLAN, plan)


def stage_html():
    p37 = L8._plan37()
    plan35 = W._s35()
    plan = F.load_plan(PLAN)
    gmap = {g["key"]: g for g in plan35["groups"]}
    REFD = OUT / "refs"
    REFD.mkdir(parents=True, exist_ok=True)

    def esc(t):
        return _html.escape(str(t or ""))

    def _rel(path_s):
        src = Path(path_s)
        if str(src).startswith(str(HERE)):
            return str(src.relative_to(HERE))
        dst = REFD / src.name
        if not dst.exists():
            shutil.copy(src, dst)
        return f"out/s40/refs/{src.name}"

    def _cand_row(rec_item, base_rel):
        vd = {v["label"]: v for v in rec_item["verdicts"]}
        row = ""
        for lab in CANDS:
            sel = rec_item["selected"] == lab
            v = vd.get(lab, {})
            row += (
                f"<figure class=cand{' sel' if sel else ''}>"
                f"<a href='{base_rel}_{lab.lower()}.png' target=_blank>"
                f"<img src='{base_rel}_{lab.lower()}.png' loading=lazy>"
                f"</a>"
                + ("<div class=selbadge>★ 선정</div>" if sel else "")
                + f"<figcaption><b>{lab} — Gemini"
                f" {v.get('score', '?')}점</b><br>"
                f"{esc(v.get('verdict_ko', ''))}</figcaption></figure>")
        return row

    def _fix_block(it, base_rel):
        if "critique" not in it:
            return ""
        if it.get("fix_skipped"):
            return ("<div class=note>✔ Gemini 결함 검사: 0건 — 선정본"
                    " 그대로 사용</div>")
        issues = "".join(
            f"<li>{esc(i['issue_ko'])} <span class=k>→"
            f" {esc(i['fix_en'])}</span></li>" for i in it["critique"])
        sel = it["selected"].lower()
        return (
            f"<div class=note>⚠ Gemini 결함 {len(it['critique'])}건 →"
            f" nb2 i2i 수정본이 최종 선정본(체인 앵커)</div>"
            f"<ul class=iss>{issues}</ul>"
            f"<div class=row>"
            f"<figure class=cmp><a href='{base_rel}_{sel}.png'"
            f" target=_blank><img src='{base_rel}_{sel}.png'"
            f" loading=lazy></a><figcaption>수정 전 (선정 원본"
            f" {it['selected']})</figcaption></figure>"
            f"<figure class=cmp fixed><a href='{base_rel}_fix.png'"
            f" target=_blank><img src='{base_rel}_fix.png'"
            f" loading=lazy></a><figcaption><b>수정 후 (i2i, 최종)</b>"
            f"</figcaption></figure></div>")

    def _refs_row(rec_item):
        return "".join(
            f"<figure class=pn><a href='{_rel(r['path'])}'"
            f" target=_blank><img src='{_rel(r['path'])}'"
            f" loading=lazy></a>"
            f"<figcaption>{esc(r['label'].split(':')[0].strip()[:70])}"
            f"<br><span class=k>{esc(Path(r['path']).name)}</span>"
            f"</figcaption></figure>" for r in rec_item.get("refs", []))

    plates_html = ""
    for tag, it in (plan.get("ext_plates") or {}).items():
        plates_html += (
            f"<div class=shot><h3>실외 bg_{tag}"
            f"<span class=k> — 랭킹 {'>'.join(it['ranking'])}</span></h3>"
            f"<div class=row>{_cand_row(it, f'out/s40/plates/bg_{tag}')}"
            f"</div>{_fix_block(it, f'out/s40/plates/bg_{tag}')}"
            f"<div class=row><div class=refs><div class=note>참조"
            f" ({len(it.get('refs', []))}장)</div>"
            f"<div class=refrow>{_refs_row(it)}</div></div></div>"
            f"<details><summary>프롬프트</summary>"
            f"<pre>{esc(it['prompt'])}</pre></details></div>")
    for key, it in (plan.get("int_plates") or {}).items():
        plates_html += (
            f"<div class=shot><h3>실내 plate_{esc(key)}"
            f"<span class=k> — 랭킹 {'>'.join(it['ranking'])}</span></h3>"
            f"<div class=row>"
            f"{_cand_row(it, f'out/s40/plates/plate_{key}')}</div>"
            f"{_fix_block(it, f'out/s40/plates/plate_{key}')}"
            f"<div class=row><div class=refs><div class=note>참조"
            f" ({len(it.get('refs', []))}장)</div>"
            f"<div class=refrow>{_refs_row(it)}</div></div></div>"
            f"<details><summary>프롬프트</summary>"
            f"<pre>{esc(it['prompt'])}</pre></details></div>")

    order = [(gk, t) for gk in W.EXT_GROUPS
             for t in gmap[gk]["shots"]] \
        + [(W.INT_GROUP, t) for t in p37["int_tags"]]
    stills = plan.get("stills", {})
    secs, body, cur = [], "", None
    for gkey, tag in order:
        if gkey != cur:
            if cur is not None:
                secs.append(body + "</section>")
            body = (f"<section><h2>{esc(gmap[gkey]['name_ko'])}"
                    f" <span class=k>({gkey})</span></h2>"
                    f"<div class=note>LOCATION lock:"
                    f" {esc(gmap[gkey]['place_en'])}</div>")
            cur = gkey
        it = stills.get(tag)
        if not it:
            body += (f"<div class=shot><h3>{tag}</h3>"
                     f"<div class=note>미생성</div></div>")
            continue
        badge = (f"<span class=on>{esc(it['ref_mode'])}"
                 + (f" (prev={esc(it['prev_used'])})"
                    if it.get("prev_used") else "")
                 + "</span>")
        usage_html = (f"<div class=note>prev 사용 지시:"
                      f" {esc(it['prev_usage'])}</div>"
                      if it.get("prev_usage") else "")
        body += (
            f"<div class=shot><h3>{tag} {badge}"
            f"<span class=k> — 랭킹 {'>'.join(it['ranking'])}</span>"
            f"</h3>{usage_html}"
            f"<div class=note>{esc(R.SHOT[R._key(tag)]['desc'])}</div>"
            f"<div class=row>{_cand_row(it, f'out/s40/stills/{tag}')}"
            f"</div>{_fix_block(it, f'out/s40/stills/{tag}')}"
            f"<div class=row><div class=refs><div class=note>참조"
            f" ({len(it.get('refs', []))}장)</div>"
            f"<div class=refrow>{_refs_row(it)}</div></div></div>"
            f"<details><summary>프롬프트 전문</summary>"
            f"<pre>{esc(it['prompt'])}</pre></details></div>")
    if cur is not None:
        secs.append(body + "</section>")

    overview = "".join(
        f"<figure class=ov>"
        f"<a href='out/s40/stills/{t}_sel.png' target=_blank>"
        f"<img src='out/s40/stills/{t}_sel.png' loading=lazy></a>"
        f"<figcaption>{t} — {esc(stills[t]['selected'])}"
        f" (Gemini {stills[t]['totals'][stills[t]['selected']]}점)"
        f"</figcaption></figure>"
        for _gk, t in order if t in stills)
    PAGE.write_text(f"""<!DOCTYPE html>
<html lang=ko><head><meta charset=utf-8>
<meta name=viewport content="width=device-width, initial-scale=1">
<title>s40 — 배경부터 스틸까지 전부 3롤+Gemini 선정</title>
<style>
body {{ margin:0; padding:24px; background:#0f1216; color:#e6e6e6;
       font:14px/1.6 -apple-system,'Apple SD Gothic Neo',sans-serif; }}
h1 {{ font-size:20px; }} h2 {{ font-size:17px; margin:34px 0 8px;
     border-bottom:1px solid #333; padding-bottom:5px; }}
h3 {{ font-size:15px; margin:6px 0; }}
.k {{ color:#8a939e; font-size:13px; font-weight:400; }}
.row {{ display:flex; gap:14px; align-items:flex-start; margin:10px 0;
        flex-wrap:wrap; }}
figure {{ margin:0; }} figcaption {{ color:#c9d2dc; font-size:12px; }}
img {{ width:100%; border-radius:8px; border:1px solid #2a2f36; }}
figure.cand {{ max-width:430px; position:relative; }}
figure.cand.sel img {{ border:4px solid #7ee2a8;
  box-shadow:0 0 14px rgba(126,226,168,.45); }}
.selbadge {{ position:absolute; top:8px; left:8px; background:#1e3a2a;
  color:#7ee2a8; border:1px solid #7ee2a8; border-radius:6px;
  padding:2px 10px; font-size:13px; font-weight:700; }}
figure.ov {{ max-width:300px; }} figure.ov img {{ max-width:300px;
  border:3px solid #7ee2a8; }}
figure.ov figcaption {{ font-size:11.5px; }}
figure.cmp {{ max-width:430px; }}
figure.cmp[fixed] img {{ border:3px solid #9ecbff; }}
ul.iss {{ margin:4px 0 8px 18px; color:#c9d2dc; font-size:13px; }}
.note {{ color:#8a939e; }}
.box {{ background:#161b22; border:1px solid #2a2f36; border-radius:8px;
       padding:12px 16px; margin:12px 0; }}
.shot {{ border-top:1px dashed #2a2f36; padding:12px 0; }}
.on {{ background:#1e3a2a; color:#7ee2a8; border-radius:5px;
      padding:1px 8px; font-size:12px; margin-left:6px; }}
.refs {{ max-width:820px; }}
.refrow {{ display:flex; gap:8px; flex-wrap:wrap; }}
figure.pn {{ max-width:150px; }} figure.pn figcaption {{ font-size:11px;
  max-width:150px; }}
details {{ margin:6px 0; }} summary {{ color:#9ecbff; cursor:pointer; }}
pre {{ background:#161b22; border:1px solid #2a2f36; padding:10px;
      white-space:pre-wrap; font-size:12px; }}
</style></head><body>
<h1>s40 — 배경(플레이트)부터 스틸 선정까지 전부 3롤 + Gemini VLM
단독 판정</h1>
<div class=box>사용자 확정 규칙: ①모든 생성 단계=<b>nb2 3롤(A/B/C) →
Gemini VLM 판정 → 선정</b>(선정본이 다음 단계 참조) ②실외 플레이트
8장=마스터 v2 참조 / 실내 플레이트 6장=FP+뿌리+직전 선정본 체이닝
③스틸 23샷 참조=<b>배경 플레이트(항상)+prev 선정본(배경 동일성 v3
판정)+캐릭터·소품(인물 샷) 모두 첨부</b>(단일 참조 아님) ④배경 전용
샷=플레이트만+NO PEOPLE ⑤<b>선정본 결함 수정 패스</b>: Gemini 가
선정본의 프롬프트 위반을 검출 → 결함 있으면 수정 프롬프트 저작 →
nb2 i2i(그 이미지 단독 참조) 수정본이 최종 선정본(체인 앵커),
수정 전/후 비교 표시. 텍스트 안전망(자세 정본·carried·
EXPRESSION_REALISM·PROP_ORIENTATION·캐릭터 traits) 유지.
이전 판: <a href='s39_threeroll_vlm.html'
style='color:#9ecbff'>s39</a> ·
<a href='s38_lightconti_full.html' style='color:#9ecbff'>s38</a></div>
<section><h2>★ 스틸 선정본 모아보기 ({len(stills)}샷)</h2>
<div class=row>{overview}</div></section>
<section><h2>플레이트 — 실외 8(마스터 v2) + 실내 6(FP 체이닝)</h2>
<div class=note>마스터=<a href='out/s38/master2.png' target=_blank
style='color:#9ecbff'>master2.png</a> · FP=<a
href='out/s37/{W.FP_FILE}' target=_blank
style='color:#9ecbff'>floorplan</a></div>
{plates_html}</section>
{''.join(secs)}
</body></html>""")
    print(f"[html] {PAGE}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", required=True,
                    choices=["ext_plates", "int_plates", "stills", "html"])
    a = ap.parse_args()
    {"ext_plates": stage_ext_plates, "int_plates": stage_int_plates,
     "stills": stage_stills, "html": stage_html}[a.only]()
