#!/usr/bin/env python3
"""本地臂 v2 两步协议: Qwen3.8-27B q3 VL (llama-server :8125)。
与 judge_v2.py (GLM-4.6V) 同 prompt 同帧, 两步盲描述→对比。
"""
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")
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}

STEP1 = """你是影视镜头分析员。给你一段视频的5帧均匀采样(按时间顺序 image1 最早 → image5 最晚)。
只描述你在画面中实际看到的内容,不要猜测任何拍摄意图。

输出 JSON(不要markdown,不要额外文字):
{"framing":"观察到的景别: 远景|中景|近景|过肩|特写|大特写|插入物件",
"framing_change":"首尾帧景别变化,如'无明显变化'或'近景→远景'",
"camera":"观察到的运镜: 固定|横移跟随|推进|拉远|俯视|其他(描述)",
"evidence_camera":"从帧间变化推断运镜的一句话证据",
"composition":"主体在画面中的位置: 三分线附近|中心|边缘(描述水平位置:左/中/右,垂直:上/中/下)",
"axis":"主体运动或朝向: 左→右|右→左|无明显横向方向",
"quality_issues":["肢体畸变/多肢/面部崩坏/服装异常/背景漂移等,无则空"],
"scene":"一句话画面内容"}"""

STEP2 = """你是影视质检员。下面是【镜头分析员的独立观察】(基于视频帧,未见过规格)和【SPEC规格】。
逐维对比观察与规格,判断视频是否符合。注意: 观察是唯一事实来源,不要让规格改写你的判断。

观察: %s

SPEC: %s

判定标准:
- framing 匹配=观察景别与spec景别同级别(远/中/近/特写/过肩/插入); 多段景别如"MCU→WS"需观察的framing_change方向一致
- camera 匹配=观察运镜与spec运镜同类型(固定/跟随/推进/拉远/俯)
- composition 匹配=观察主体位置符合spec构图意图(三分法/引导线)
- axis 匹配=观察方向与spec方向一致; 若观察为"无明显横向方向"则axis_ok=true仅当spec为中立

输出 JSON(不要markdown,不要额外文字):
{"framing_ok":bool,"camera_ok":bool,"composition_ok":bool,"axis_ok":bool,
"confidence":0.0-1.0,"verdict":"pass|fail","reasons":"一句话核心依据"}"""

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

def call_api(content):
    body = json.dumps({"model": "qwen3.8", "temperature": 0.1, "max_tokens": 800,
        "messages": [{"role": "user", "content": content}]}).encode()
    req = urllib.request.Request(API, data=body, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=600) as r:
        return json.load(r)['choices'][0]['message']['content'].strip()

def parse_json(txt):
    m = re.search(r'\{.*\}', txt, re.S)
    try: return json.loads(m.group(0)) if m else None
    except Exception: return None

def judge_v2(shot_id, spec):
    d = FR / shot_id
    imgs = [{"type": "image_url", "image_url": {"url": b64url(d / f)}}
            for f in ['f0.jpg','f1.jpg','f2.jpg','f3.jpg','f4.jpg']]
    t0 = time.time()
    obs_txt = call_api([{"type": "text", "text": STEP1}] + imgs)
    obs = parse_json(obs_txt)
    spec_slim = json.dumps({k: spec.get(k) for k in
        ['shot_id','shot_type','framing','camera_movement','composition','axis_line']},
        ensure_ascii=False)
    cmp_txt = call_api([{"type": "text", "text": STEP2 % (obs_txt, spec_slim)}])
    cmp = parse_json(cmp_txt)
    return {"video": shot_id, "spec_of": spec.get('shot_id'),
            "elapsed_s": round(time.time()-t0,1), "obs": obs,
            "verdict": cmp.get('verdict') if cmp else "PARSE_FAIL",
            "dims": {k: cmp.get(k) for k in ['framing_ok','camera_ok','composition_ok','axis_ok']} if cmp else None,
            "reasons": cmp.get('reasons') if cmp else cmp_txt[:200]}

CONTROLS = [
    ("S1-01", "S4-01", "强反差:WS远景跟随 vs CU特写推进"),
    ("S5-03", "S8-01", "强反差:CU特写固定 vs 远景拉远"),
    ("S7-03", "S6-02", "强反差:WS跟随 vs CU推进"),
    ("S4-02", "S2-02", "强反差:MCU固定 vs WS跟随"),
    ("S8-01", "S1-03", "强反差:远景拉远 vs INSERT特写"),
    ("S3-01", "S1-02", "中反差:FS中景 vs MCU过肩"),
    ("S1-02", "S2-01", "仅运镜:固定 vs 缓慢拉远"),
    ("S7-02", "S5-03", "仅运镜:缓慢推进 vs 固定"),
]

def main():
    mode = sys.argv[1] if len(sys.argv) > 1 else "full"
    log = open(OUT / 'judge_v2_local.jsonl', 'a')
    tasks = []
    if mode == "controls":
        tasks = [("neg", v, spec_by_id[s]) for v, s, _ in CONTROLS]
    elif mode == "full":
        tasks = [("pos", s['shot_id'], s) for s in shots]
        tasks += [("neg", v, spec_by_id[s]) for v, s, _ in CONTROLS]
    for i, (tag, vid, spec) in enumerate(tasks):
        try:
            r = judge_v2(vid, spec); r['tag'] = tag
        except Exception as e:
            r = {"tag": tag, "video": vid, "spec_of": spec.get('shot_id'), "error": str(e)[:300]}
        log.write(json.dumps(r, ensure_ascii=False) + "\n"); log.flush()
        print(f"[{i+1}/{len(tasks)}] {tag} v={vid} s={r.get('spec_of')} -> {r.get('verdict','ERR')} | {str(r.get('reasons',''))[:80]}", flush=True)
    print("DONE")

if __name__ == '__main__':
    main()
