"""density_check.py — 分镜密度自检 gate（防长片退化成 PPT 说教）。

用法:
    py -3.14 scripts/density_check.py <storyboard.json> <timing.json>

读分镜动词分布 + 音频时长，算视觉密度/运动占比/说教占比/角色数/静止幕，
低于阈值打印警告并 exit 1（达标 exit 0）。把"长片要密、要动、要多角色"
从会忘的纪律变成管线必过的检查。阈值由 econ(好,36s) 通过 / biz(退化,173s) 报警标定。
"""
from __future__ import annotations
import json
import sys
from collections import Counter

# 运动叙事：角色/物体真正在画面里位移、传递、翻滚——撑起画面的主菜。
MOTION = {"walk_to", "run_to", "give", "hops", "emerge", "orbit", "jump", "spin"}
# 原地说教手势：调味料，过量即沦为 boss 站桩指点。
TALK = {"point", "nod", "shake_head", "emote", "look", "wave"}
# 其余（bounce/pose/grow/show/hide/appear/vanish/fx/place）记入总数但不归两类。

# 阈值（econ 全过 / biz 多项触警）
T_DENSITY = 1.2      # 动词/秒下限
T_MOTION = 15        # 运动占比%下限
T_TALK = 35          # 说教占比%上限
T_CAST_LONG = 3      # 长片(>60s)角色数下限
T_LONG_SEC = 60


def main(sb_path: str, timing_path: str) -> int:
    sb = json.load(open(sb_path, encoding="utf-8"))
    timing = json.load(open(timing_path, encoding="utf-8"))
    dur = float(timing["audio_duration"])

    verbs = [a["verb"] for s in sb.get("scenes", []) for a in s.get("actions", [])]
    n = len(verbs)
    c = Counter(verbs)
    cast = len(sb.get("cast", {}))

    motion = sum(c[v] for v in MOTION)
    talk = sum(c[v] for v in TALK)
    density = n / dur if dur else 0
    motion_pct = motion / n * 100 if n else 0
    talk_pct = talk / n * 100 if n else 0

    # 静止幕：整幕只有 emote（连画面事件都没有）
    visual = MOTION | {"bounce", "pose", "grow", "show", "hide", "appear", "vanish", "fx", "point", "nod", "shake_head", "jump", "spin", "wave"}
    weak = [s.get("plan_scene", "?") for s in sb.get("scenes", [])
            if not any(a["verb"] in visual for a in s.get("actions", []))]

    is_long = dur > T_LONG_SEC
    warns: list[str] = []
    if density < T_DENSITY:
        warns.append(f"视觉密度 {density:.2f} 动词/秒 < {T_DENSITY}（画面太空，旁白期间大量留白）")
    if motion_pct < T_MOTION:
        warns.append(f"运动叙事占比 {motion_pct:.0f}% < {T_MOTION}%（缺 walk/run/give/hops，角色在站桩）")
    if talk_pct > T_TALK:
        warns.append(f"说教手势占比 {talk_pct:.0f}% > {T_TALK}%（point/nod/emote 泛滥，沦为指点解说）")
    if is_long and cast < T_CAST_LONG:
        warns.append(f"长片({dur:.0f}s) 角色仅 {cast} 个 < {T_CAST_LONG}（独角戏，需多角色互动）")
    if is_long and c.get("give", 0) == 0:
        warns.append("长片 give=0（贯穿物没有在角色间流动，缺传递叙事）")
    if weak:
        warns.append(f"{len(weak)} 幕只有表情、无任何画面事件: {weak}")

    print(f"=== 密度自检  时长{dur:.0f}s / {n}动词 / {cast}角色 ===")
    print(f"密度 {density:.2f}/s | 运动 {motion_pct:.0f}% | 说教 {talk_pct:.0f}% | give×{c.get('give',0)} walk×{c.get('walk_to',0)} run×{c.get('run_to',0)}")
    top = " ".join(f"{v}×{k}" for v, k in c.most_common(8))
    print(f"主要动词: {top}")
    if warns:
        print("\n[WARN] 分镜偏向 PPT 说教，修改后重跑：")
        for w in warns:
            print(f"  - {w}")
        return 1
    print("\n[OK] 密度达标，画面够密够动。")
    return 0


if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: py -3.14 scripts/density_check.py <storyboard.json> <timing.json>", file=sys.stderr)
        sys.exit(2)
    sys.exit(main(sys.argv[1], sys.argv[2]))
