#!/usr/bin/env python3
"""s39 — 옥탑 23샷: 콘티 제외·참조 최소화 + 3롤 VLM 선택 (2026-07-13).

사용자 지시(s38 후속): "콘티 제외하고, 각 샷별로 이전 샷과 배경만
경우에 따라서 판단해서 각각 생성. 대신 3장씩 생성해서 VLM 으로
프롬프트를 가장 잘 따른 것을 판단해 선택. 새 갤러리에 모든 이미지와
GPT/Gemini VLM 판정 결과를 보여줘."

- 참조 = 단 한 장: prev2 v3 판정(배경 동일성 유일 기준, s38 재사용)이
  prev 를 주면 이전 샷의 '선택본' 실사 / 아니면 배경 플레이트.
  콘티·캐릭터·소품 참조 전부 제외.
- 샷당 nb2 3롤(A/B/C) → GPT+Gemini VLM 이 프롬프트(+참조 일관성)
  충실도 0-10 판정 → 합산 최고 선택(동점=Gemini 랭킹, s29 계약).
- 선택본({tag}_sel.png)이 후속 샷의 prev 앵커 → 스토리 순서 순차 생성.

사용: backend/.venv/bin/python s39_threeroll_vlm.py --only <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

OUT = W.OUT.parent / "s39"
OUTS = OUT / "stills"
PAGE = HERE / "s39_threeroll_vlm.html"
PLAN = "s39_threeroll_v1"
CANDS = ["A", "B", "C"]

# 범용 소품 방향 계약 — 기능면(화면·앞면·페이지)의 스테이징 (시나리오 무관)
PROP_ORIENTATION = "\n".join([
    "PROPS FACE THE RIGHT WAY: every handheld or used object must be",
    "oriented exactly as its real-world use requires. A person reading,",
    "watching or operating something (a phone, a photograph, a paper,",
    "any device) has its functional side — screen, front, page — facing",
    "THEIR OWN eyes; the camera then sees whatever side the staging",
    "geometry implies (often its back). Show the functional side to the",
    "camera ONLY when the shot text itself stages it toward the viewer.",
    "Never flip, mirror or reverse an object's front and back.",
])

JUDGE_SYSTEM = "\n".join([
    "You are given ONE image-generation prompt, the REFERENCE image(s)",
    "that were attached to it (labelled), and THREE candidate images,",
    "labelled A, B, C in the order attached, all generated from that",
    "exact prompt with those exact references.",
    "Pick the ONE candidate that most faithfully realises the prompt.",
    "Judge ONLY fidelity to the prompt and its reference instructions:",
    "the shot text's moment and action, who is and is NOT in frame,",
    "camera framing, time of day, the LOCATION lock (the place must be",
    "the one the reference shows — same architecture, materials,",
    "fixtures), pose/immobility and carried-state clauses, and every",
    "exclusion (no text, no invented people or objects). Consistency",
    "with the attached reference image counts as prompt fidelity.",
    "Ignore generic aesthetic appeal.",
    "Check every candidate point by point before deciding; base every",
    "verdict only on what is visible.",
    "Also give every candidate an integer score 0-10 for that same",
    "fidelity.",
    "Output: winner, ranking best-to-worst, and per candidate a score",
    "plus a one-line Korean verdict citing the decisive prompt points.",
])


_TRAITS_CACHE = None


def _char_traits():
    """캐릭터 short_id → EN traits 문자열 (entity_canon.stable_traits)."""
    global _TRAITS_CACHE
    if _TRAITS_CACHE is None:
        import json as _json
        import os
        import subprocess
        sql = ("SELECT short_id, stable_traits FROM entity_canon"
               f" WHERE project_id='{F.PROJECT_ID}'"
               " AND entity_type='character'")
        env = {**os.environ, "PGPASSWORD": "theroad_dev_2026"}
        out = subprocess.run(
            ["psql", "-h", "localhost", "-U", "theroad", "-d", "theroad",
             "-tAc", sql], capture_output=True, text=True, env=env,
            check=True).stdout
        _TRAITS_CACHE = {}
        for line in out.strip().splitlines():
            if "|" not in line:
                continue
            sid, traits = line.split("|", 1)
            try:
                _TRAITS_CACHE[sid.strip()] = ", ".join(
                    _json.loads(traits))
            except Exception:
                pass
    return _TRAITS_CACHE


def _plate_of(tag, gkey, p37):
    if gkey != W.INT_GROUP:
        for pfx_dir, name in ((L8.OUT, f"bg4_{tag}.png"),
                              (L8.OUT, f"bg3_{tag}.png"),
                              (L8.OUT, f"bg2_{tag}.png"),
                              (W.OUT, f"bg_{tag}.png")):
            if (pfx_dir / name).exists():
                return pfx_dir / name
        raise SystemExit(f"플레이트 없음: {tag}")
    for p_ in p37["int_plates"]:
        if tag in p_["covers"]:
            return W.OUT / f"plate_{p_['key']}.png"
    raise SystemExit(f"실내 플레이트 미배정: {tag}")


def _build_prompt(tag, gkey, p37, mv, gmap, prev_used, prev_usage,
                  bg_only=False):
    s = R.SHOT[R._key(tag)]
    fix = p37.get("pose_fix", {}).get(tag) or {}
    m = mv.get(tag, {})
    movement = fix.get("movement_en") or m.get("movement", "")
    figures = fix.get("figures_en") or m.get("figures", "")
    carried = fix.get("carried_en") or (
        p37.get("int_cont", {}).get(tag, {}).get("carried_en", "")
        if gkey == W.INT_GROUP else "")
    chars = S._char_refs()
    ve = S._ve_ids()
    scene_union = {}
    for (si, _shi), ids in ve.items():
        scene_union.setdefault(si, set()).update(ids)
    ve_ids = ve.get(R._key(tag), [])
    if not ve_ids:
        ve_ids = sorted(scene_union.get(R._key(tag)[0], set()))
    traits = _char_traits()
    char_names = [
        chars[cid][0]
        + (f" ({traits[cid]})" if traits.get(cid) else "")
        for cid in ve_ids if cid in chars]
    parts = [
        "Create ONE FINAL photorealistic live-action film still of"
        " the moment below — contemporary South Korea, 2026; all"
        " people are Korean unless stated. TIME OF DAY (lock): "
        + S._time_of(tag) + ".",
        f"SHOT TEXT (authoritative, Korean): {S._soften(s['desc'])}",
        f"LOCATION (lock): {gmap[gkey]['place_en']} The shot takes"
        " place here — the attached "
        + ("PREVIOUS SHOT STILL shows this exact place."
           if prev_used else "LOCATION PHOTOGRAPH shows the exact"
           " spot."),
    ]
    if prev_used:
        parts.append(
            "THIS SHOT CONTINUES THE PREVIOUS SHOT: everything the"
            " attached still established — the place, its fixed"
            " features and wear, each person's clothing and state —"
            " persists. Any person in it who cannot move stays"
            " PRECISELY as photographed (body, pose, contact points,"
            " held objects); only the camera changes.")
        if prev_usage:
            parts.append(
                "PREVIOUS STILL USAGE (follow exactly — what to take"
                " from the attached still and what to exclude): "
                + S._soften(prev_usage))
    # 배경 전용 샷엔 인물 관련 절(자세 정본) 주입 금지 — 인물 유도 방지
    pc = [] if bg_only else L8._pose_clauses(p37, [tag])
    if pc:
        parts.append("\n".join(pc))
    parts.append(S.REALIZE_STILL)
    parts.append(L8.EXPRESSION_REALISM)
    parts.append(PROP_ORIENTATION)
    if movement:
        parts.append("MOVEMENT (follow exactly): " + S._soften(movement))
    if figures:
        parts.append("FIGURES — size & depth (follow exactly): "
                     + S._soften(figures))
    if carried:
        parts.append("CARRIED STATE (persist exactly — must match"
                     " the neighbouring shots of this scene): "
                     + S._soften(carried))
    if bg_only:
        parts.append(
            "NO PEOPLE IN THIS SHOT: the shot text shows only the place"
            " and its state — no living person or any body part appears"
            " in frame, unless the shot text itself explicitly says so.")
    elif char_names:
        parts.append(
            "PEOPLE: the SHOT TEXT alone decides whether any person"
            " is visible in this shot. IF a person appears, they must"
            " be one of: " + "; ".join(char_names)
            + " — never anyone else, and never add a person the shot"
            " text does not show. IF only part of a person is in frame"
            " (a hand, arm, foot, back, silhouette), that body part"
            " belongs to the specific person the shot text names — its"
            " sex, age, build, skin and grooming must unmistakably"
            " match that person's profile above.")
    else:
        parts.append("No people appear unless the shot text itself"
                     " says so.")
    parts.append("No text, captions, watermarks or annotations"
                 " anywhere.")
    return "\n\n".join(parts)


def _gemini_select(judge):
    """Gemini 점수 단독 선정 — 동점=Gemini 랭킹."""
    gem = judge["gemini-pro"]
    totals = {v["label"]: v["score"] for v in gem["verdicts"]}
    best = max(totals.values())
    tied = [lab for lab, t in totals.items() if t == best]
    rank = gem["ranking"]
    selected = min(tied, key=lambda lab: rank.index(lab)
                   if lab in rank else 99)
    return totals, selected


def stage_bgonly():
    """배경 전용 샷 판정 (07-13 사용자: "S14sh5 같이 배경만 있는 샷은
    앞쪽 샷 참조 말고 배경 기반으로") — 실물 인물이 프레임에 없는
    샷은 prev 대신 플레이트를 참조한다."""
    p37 = L8._plan37()
    tags = L8._all_tags_story_order(p37)
    SYS = "\n".join([
        "당신은 콘티 판독가다. 샷 목록을 받는다. 각 샷마다, 샷 텍스트가",
        "묘사하는 프레임 안에 '살아있는 인물의 실물 몸'이 직접 등장하는지",
        "판정하라 (person_visible).",
        "· 신체 일부(손·손목·발·뒷모습)만 보여도 등장이다.",
        "· 거울·수면 등 반사상에 인물이 보이는 것도 등장으로 친다.",
        "· 다음은 등장이 아니다: 벽·바닥·가구 등 공간의 상태만 보여주는",
        "  샷, 인물의 그림자만 맺힌 샷, 사진·액자·화면 '속' 인물 이미지만",
        "  보이는 샷, 발자국 같은 흔적만 있는 샷.",
        "샷 텍스트에 적힌 것만 근거로 판단하라. 각 샷: shot,",
        "person_visible(bool), reason_ko(한 구절).",
    ])
    SCHEMA = {"type": "object", "additionalProperties": False,
              "properties": {"items": {"type": "array", "items": {
                  "type": "object", "additionalProperties": False,
                  "properties": {
                      "shot": {"type": "string"},
                      "person_visible": {"type": "boolean"},
                      "reason_ko": {"type": "string"}},
                  "required": ["shot", "person_visible", "reason_ko"]}}},
              "required": ["items"]}
    shots_txt = "\n".join(
        f"{t}: {R.SHOT[R._key(t)]['desc']}" for t in tags)
    res = F.llm("s39_bgonly", SYS, f"샷 목록:\n{shots_txt}", SCHEMA)
    got = {it["shot"]: it for it in res["items"]}
    missing = [t for t in tags if t not in got]
    if missing:
        raise SystemExit(f"bgonly 누락: {missing}")
    plan = F.load_plan(PLAN)
    plan["bgonly"] = got
    F.save_plan(PLAN, plan)
    for t in tags:
        it = got[t]
        mark = "인물" if it["person_visible"] else "★배경 전용"
        print(f"[bgonly] {t}: {mark} | {it['reason_ko']}")


def stage_reselect():
    """기저장 판정에서 Gemini 점수만으로 재선정 + _sel.png 갱신."""
    plan = F.load_plan(PLAN)
    changed = []
    for tag, mt in plan["shots"].items():
        old = mt["selected"]
        totals, selected = _gemini_select(mt["judge"])
        mt["totals"] = totals
        mt["selected"] = selected
        shutil.copy(OUTS / f"{tag}_{selected.lower()}.png",
                    OUTS / f"{tag}_sel.png")
        if selected != old:
            changed.append((tag, old, selected))
        print(f"[reselect] {tag}: gemini={totals} sel={selected}"
              + (f" (변경 {old}→{selected})" if selected != old else ""))
    F.save_plan(PLAN, plan)
    print(f"[reselect] 변경 {len(changed)}건: {changed}")


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).get("prev2") or {}
    assert prev2, "s38 prev2 판정 없음 — 먼저 s38 --only prev2"
    plan = F.load_plan(PLAN) if (HERE / "plans" / f"{PLAN}.json").exists() \
        else {}
    meta = plan.setdefault("shots", {})
    OUTS.mkdir(parents=True, exist_ok=True)
    for tag in L8._all_tags_story_order(p37):
        sel_out = OUTS / f"{tag}_sel.png"
        if sel_out.exists():
            print(f"[s39] {tag} skip(exists)")
            continue
        gkey = tag2g[tag]
        pj = prev2.get(tag) or {}
        prev_tag = pj.get("prev")
        prev_usage = pj.get("usage_en", "")
        # ★배경 전용 샷(실물 인물 無)은 prev 대신 플레이트 (사용자 규칙)
        bg_forced = False
        bgo = (plan.get("bgonly") or {}).get(tag)
        if prev_tag and bgo and not bgo["person_visible"]:
            prev_tag, bg_forced = None, True
        prev_used = None
        if prev_tag and (OUTS / f"{prev_tag}_sel.png").exists():
            prev_used = prev_tag
            refs = [(W.PREV_STILL_LABEL, OUTS / f"{prev_tag}_sel.png")]
        else:
            refs = [(W.PLATE_STILL_LABEL, _plate_of(tag, gkey, p37))]
        bg_only = bool(bgo) and not bgo["person_visible"]
        prompt = _build_prompt(tag, gkey, p37, mv, gmap,
                               prev_used, prev_usage, bg_only=bg_only)
        for lab in CANDS:
            F.img_nb2(f"s39_{tag}_{lab.lower()}", prompt, refs,
                      aspect_ratio="16:9",
                      out_path=OUTS / f"{tag}_{lab.lower()}.png")
        # VLM 이중 판정 — 프롬프트+참조+후보 3장
        parts = [{"type": "text",
                  "text": "THE PROMPT (all three candidates were"
                          " generated from this):\n" + prompt}]
        for rlab, rp in refs:
            parts.append({"type": "text",
                          "text": f"REFERENCE — {rlab}"})
            parts.append(F.png_data_url(rp))
        for lab in CANDS:
            parts.append({"type": "text", "text": f"Candidate {lab}:"})
            parts.append(F.png_data_url(OUTS / f"{tag}_{lab.lower()}.png"))
        # ★판정=Gemini 단독 (07-13 사용자: "gemini 로만 하자 gpt 이상해")
        judge = {}
        for model, jtag in (("gemini-pro", f"s39_judge_gemini_{tag}"),):
            judge[model] = F.llm(jtag, JUDGE_SYSTEM, parts,
                                 W.T.JUDGE_SCHEMA, model=model)
        totals, selected = _gemini_select(judge)
        shutil.copy(OUTS / f"{tag}_{selected.lower()}.png", sel_out)
        meta[tag] = {
            "gkey": gkey, "prev_used": prev_used,
            "prev_usage": prev_usage if prev_used else "",
            "bg_forced": bg_forced,
            "ref_mode": ("prev 선택본" if prev_used
                         else "배경 플레이트 (★배경 전용 샷 — prev 무시)"
                         if bg_forced else "배경 플레이트"),
            "refs": [{"label": lab_, "path": str(p_)}
                     for lab_, p_ in refs],
            "prompt": prompt, "judge": judge, "totals": totals,
            "selected": selected,
        }
        F.save_plan(PLAN, plan)
        print(f"[s39] {tag} 완료 mode={meta[tag]['ref_mode']}"
              f" prev={prev_used} totals={totals} sel={selected}")


def stage_html():
    p37 = L8._plan37()
    plan35 = W._s35()
    plan = F.load_plan(PLAN)
    gmap = {g["key"]: g for g in plan35["groups"]}
    meta = plan.get("shots", {})
    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/s39/refs/{src.name}"

    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"]]
    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
        mt = meta.get(tag)
        if not mt:
            body += (f"<div class=shot><h3>{tag}</h3>"
                     f"<div class=note>미생성</div></div>")
            continue
        badge = (f"<span class=on>prev={esc(mt['prev_used'])}</span>"
                 if mt.get("prev_used")
                 else "<span class=off>배경 전용 샷 → 플레이트 강제</span>"
                 if mt.get("bg_forced")
                 else "<span class=off>배경 플레이트</span>")
        usage_html = (f"<div class=note>사용 지시:"
                      f" {esc(mt['prev_usage'])}</div>"
                      if mt.get("prev_usage") else "")
        gv = {v["label"]: v
              for v in mt["judge"]["gemini-pro"]["verdicts"]}
        cands = ""
        for lab in CANDS:
            sel = mt["selected"] == lab
            m_ = gv.get(lab, {})
            cands += (
                f"<figure class=cand{' sel' if sel else ''}>"
                f"<a href='out/s39/stills/{tag}_{lab.lower()}.png'"
                f" target=_blank><img"
                f" src='out/s39/stills/{tag}_{lab.lower()}.png'"
                f" loading=lazy></a>"
                + ("<div class=selbadge>★ 선정</div>" if sel else "")
                + f"<figcaption><b>{lab} — Gemini"
                f" {m_.get('score', '?')}점</b><br>"
                f"{esc(m_.get('verdict_ko', ''))}"
                f"</figcaption></figure>")
        gj = mt["judge"]["gemini-pro"]
        winners = (f"Gemini winner={gj['winner']}"
                   f" 랭킹={'>'.join(gj['ranking'])}")
        ref_items = "".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())}"
            f"</figcaption></figure>" for r in mt.get("refs", []))
        body += (
            f"<div class=shot><h3>{tag} {badge}"
            f"<span class=k> — {esc(winners)}</span></h3>{usage_html}"
            f"<div class=note>{esc(R.SHOT[R._key(tag)]['desc'])}</div>"
            f"<div class=row>{cands}</div>"
            f"<div class=row><div class=refs><div class=note>참조"
            f" (1장) — {esc(mt['ref_mode'])}</div>"
            f"<div class=refrow>{ref_items}</div></div></div>"
            f"<details><summary>프롬프트 전문</summary>"
            f"<pre>{esc(mt.get('prompt', ''))}</pre></details></div>")
    if cur is not None:
        secs.append(body + "</section>")

    n_prev = sum(1 for m in meta.values() if m.get("prev_used"))
    overview = "".join(
        f"<figure class=ov>"
        f"<a href='out/s39/stills/{t}_sel.png' target=_blank>"
        f"<img src='out/s39/stills/{t}_sel.png' loading=lazy></a>"
        f"<figcaption>{t} — {esc(meta[t]['selected'])}"
        f" (Gemini {meta[t]['totals'][meta[t]['selected']]}점)"
        f"</figcaption></figure>"
        for _gk, t in order if t in meta)
    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>s39 — 옥탑 23샷: 3롤 + Gemini VLM 선정</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; }}
.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; }}
.off {{ background:#3a2a1e; color:#e2b57e; border-radius:5px;
       padding:1px 8px; font-size:12px; margin-left:6px; }}
.refs {{ max-width:640px; }}
.refrow {{ display:flex; gap:8px; flex-wrap:wrap; }}
figure.pn {{ max-width:170px; }} figure.pn figcaption {{ font-size:11px;
  max-width:170px; }}
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>s39 — 옥탑 23샷: 콘티 제외·참조 1장 + 3롤 <b>Gemini VLM 단독
선정</b> (prev {n_prev} / 플레이트 {len(meta) - n_prev})</h1>
<div class=box>사용자 확정 규칙: ①<b>콘티 제외</b> — 참조는 단 한 장:
prev 판정 v3(배경 동일성 유일 기준, s38 재사용)가 prev 를 주면
<b>이전 샷 선정본</b> / 아니면 <b>배경 플레이트</b>.
②샷당 <b>nb2 3롤(A/B/C)</b> → <b>★Gemini VLM 단독</b>이 프롬프트
(+참조 일관성) 충실도 0-10 판정 → 최고점 선정(동점=Gemini 랭킹).
GPT 판정은 사용자 지시로 평가에서 제외. ③선정본(초록 테두리+★)이
후속 샷의 prev 앵커(스토리 순서 체인). 자세 정본·carried state·
EXPRESSION_REALISM 텍스트 안전망 유지.
이전 판: <a href='s38_lightconti_full.html'
style='color:#9ecbff'>s38 갤러리</a></div>
<section><h2>★ 선정본 모아보기 (Gemini 점수 단독, {len(meta)}샷)</h2>
<div class=row>{overview}</div></section>
{''.join(secs)}
</body></html>""")
    print(f"[html] {PAGE}")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("--only", required=True,
                    choices=["bgonly", "stills", "reselect", "html"])
    a = ap.parse_args()
    {"bgonly": stage_bgonly, "stills": stage_stills,
     "reselect": stage_reselect, "html": stage_html}[a.only]()
