#!/usr/bin/env python3
"""qwen-eye: 用 Qwen3.8-27B VL (本地 llama-server :8125) 评判 P11 视频是否符合 shot spec。
实验设计:
  - 20 个正配对 (spec↔video 原配) → 期望 pass
  - 8 个负对照 (交叉配对, GT=必 fail): 6 个强反差 + 2 个仅运镜不符 (灵敏度测试)
  - 3 个重复调用 (稳定性测试)
输出: results/judge_raw.jsonl (逐条) + results/report.json (汇总)
"""
import json, base64, time, sys, re
from pathlib import Path
import urllib.request

EP = Path("/home/kai/runs/ep-cat-worker-001")
FR = Path("/home/kai/shared/qwen-eye-exp/frames")
OUT = Path("/home/kai/shared/qwen-eye-exp/results")
OUT.mkdir(exist_ok=True)
API = "http://127.0.0.1:8125/v1/chat/completions"

shots = json.load(open(EP / '.pipeline-assets/shot-list.json'))['value']
spec_by_id = {s['shot_id']: s for s in shots}

PROMPT = """你是影视级AI视频质检员。给你【镜头规格 SPEC】和该镜头视频的5帧均匀采样(按时间顺序 image1 最早 → image5 最晚)。逐维判断视频是否符合规格。

判定标准:
- framing(景别): 远景WS=环境为主人物很小 / 中景FS/MS=膝~腰上 / 近景MCU=胸部以上 / 过肩OTS=越肩看对方 / 特写CU/BCU=脸部或局部充满画面 / INSERT=插入的局部或物件
- camera(运镜): 固定=画面框基本不动 / 跟随=主体保持画面中背景持续流动 / 缓慢推进=视野收窄主体渐大 / 缓慢拉远=视野扩大主体渐小 / 下俯=俯视角
- composition(构图): 三分法=主体位于三分线或交点附近 / 引导线=画面线条(建筑/道路/站台)引导视线指向主体
- axis(轴线): 左→右=主体运动或朝向从左向右 / 右→左=反向 / 中立=无明显横向方向
- 多段景别如 "MCU→WS" 取首尾帧变化判断; "MCU/CU montage" 取主导类型

严格输出以下 JSON（不要 markdown 代码块, 不要额外文字）:
{"shot_id":"...","framing_ok":bool,"camera_ok":bool,"composition_ok":bool,"axis_ok":bool,"quality_issues":["肢体畸变/多肢/面部崩坏/服装异常/背景漂移等,无则空"],"confidence":0.0-1.0,"verdict":"pass|fail","reasons":"一句话核心依据"}

SPEC: """

def b64(p):
    return "data:image/jpeg;base64," + base64.b64encode(Path(p).read_bytes()).decode()

def call(shot_id, spec, tag):
    d = FR / shot_id
    content = [{"type": "text", "text": PROMPT + json.dumps({k: spec.get(k) for k in
        ['shot_id','shot_type','framing','camera_movement','composition','axis_line','intent']},
        ensure_ascii=False)}]
    for f in ['f0.jpg', 'f1.jpg', 'f2.jpg', 'f3.jpg', 'f4.jpg']:
        content.append({"type": "image_url", "image_url": {"url": b64(d / f)}})
    body = json.dumps({
        "model": "qwen3.8", "temperature": 0.1, "max_tokens": 600,
        "messages": [{"role": "user", "content": content}]
    }).encode()
    t0 = time.time()
    req = urllib.request.Request(API, data=body, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=600) as r:
        resp = json.load(r)
    dt = time.time() - t0
    txt = resp['choices'][0]['message']['content'].strip()
    m = re.search(r'\{.*\}', txt, re.S)
    try:
        parsed = json.loads(m.group(0)) if m else None
    except Exception:
        parsed = None
    return {"tag": tag, "video": shot_id, "spec_of": spec.get('shot_id'), "elapsed_s": round(dt, 1),
            "raw": txt, "parsed": parsed}

# ---- 负对照设计: (video, 错配spec, 期望) ----
CONTROLS = [
    ("S1-01", "S4-01", "fail", "强反差:WS远景跟随 vs CU特写推进"),
    ("S5-03", "S8-01", "fail", "强反差:CU特写固定 vs 远景拉远"),
    ("S7-03", "S6-02", "fail", "强反差:WS跟随 vs CU推进"),
    ("S4-02", "S2-02", "fail", "强反差:MCU固定 vs WS跟随"),
    ("S8-01", "S1-03", "fail", "强反差:远景拉远 vs INSERT特写"),
    ("S3-01", "S1-02", "fail", "中反差:FS中景 vs MCU过肩"),
    ("S1-02", "S2-01", "fail", "仅运镜:固定 vs 缓慢拉远"),
    ("S7-02", "S5-03", "fail", "仅运镜:缓慢推进 vs 固定"),
]
REPEATS = ["S1-01", "S4-01", "S7-03"]  # 稳定性: 每个跑2次

def main():
    only = sys.argv[1] if len(sys.argv) > 1 else None
    log = open(OUT / 'judge_raw.jsonl', 'a')
    tasks = []
    if only in (None, 'pos'):
        for s in shots: tasks.append(("pos", s['shot_id'], spec_by_id[s['shot_id']]))
    if only in (None, 'neg'):
        for vid, sid, _, _ in CONTROLS: tasks.append(("neg", vid, spec_by_id[sid]))
    if only in (None, 'rep'):
        for vid in REPEATS: tasks.append(("rep", vid, spec_by_id[vid]))
    for i, (tag, vid, spec) in enumerate(tasks):
        rec = call(vid, spec, tag)
        log.write(json.dumps(rec, ensure_ascii=False) + "\n"); log.flush()
        p = rec['parsed']
        verdict = p.get('verdict') if p else "PARSE_FAIL"
        print(f"[{i+1}/{len(tasks)}] {tag:3} video={vid} spec={rec['spec_of']} -> {verdict} ({rec['elapsed_s']}s)", flush=True)
    log.close()
    print("DONE")

if __name__ == '__main__':
    main()
