#!/usr/bin/env python3
"""用 Qwen3.8-27B VL 智能眼"看"任意视频 (无 SPEC 版):
观察层: 5帧逐图 (每图一条 user 消息, 绕 llama.cpp 多图丢失 bug)
综合层: 观察 + 视频标题 → 内容理解 + 运镜/构图/一致性/缺陷 + 标题匹配
"""
import json, base64, time, re, urllib.request
from pathlib import Path

API = "http://127.0.0.1:8125/v1/chat/completions"
FR = Path("/home/kai/shared/qwen-eye-exp/frames_catvideo")
TITLE = "老板让你帮忙搬东西时的你"
SERIES = "打工瞄"
OUT = Path("/home/kai/shared/qwen-eye-exp/results/catvideo_report.json")

OBS = """以下是同一视频按时间序的5帧。逐帧用自然语言描述你实际看到的:
1. 主体是什么(人/动物/物体), 什么颜色外观?
2. 主体位置(左/中/右)和大小占比?
3. 从第1帧到第5帧, 主体大小/位置/朝向/背景有何变化?
4. 有无明显画面缺陷(畸形/糊化/黑屏/水印)?
5. 综合: 景别(远/全/中/近/特写)? 运镜(固定/跟随/推进/拉远)?
直接描述,不要JSON。"""

SUM_TMPL = """你是影视质检员兼内容分析师。下面是【镜头分析员对视频5帧的独立观察】(自然语言, 唯一事实)。
视频来自B站合集《{series}》, 本集标题: 《{title}》(竖屏短视频)。

请输出:
1. **内容梗概**: 这30秒讲了什么? 角色是谁? 情绪/笑点在哪?
2. **运镜分析**: 有哪些镜头切换? 每段运镜方式?
3. **构图分析**: 主体位置/画面平衡/字幕条位置?
4. **一致性**: 角色外观跨帧是否一致? 场景是否连贯?
5. **缺陷**: 畸形/糊化/黑屏/水印/字幕错位?
6. **标题匹配度**: 内容与标题《{title}》是否相符? 简短理由。
严格输出JSON(无markdown):
{{"summary":"内容梗概2-3句","shots":["逐镜头: 00:0X-00:0X 景别/运镜/内容"],"camera":"运镜综述1句","composition":"构图综述1句","consistency":"角色/场景一致性综述","defects":"缺陷或无","title_match":bool,"title_reason":"1句","overall_quality":"1-10分+1句理由"}}

观察: <OBS>"""

def b64(p): return base64.b64encode(Path(p).read_bytes()).decode()

def call(msgs, n=2000):
    body = json.dumps({"model": "qwen3.8", "temperature": 0.1, "max_tokens": n,
        "chat_template_kwargs": {"enable_thinking": False},
        "messages": msgs}).encode()
    req = urllib.request.Request(API, data=body, headers={"Content-Type": "application/json"})
    for a in range(2):
        try:
            with urllib.request.urlopen(req, timeout=3600) as r:
                resp = json.load(r)
            txt = (resp['choices'][0]['message'].get('content') or '').strip()
            if txt: return txt, resp['usage']
        except Exception:
            if a == 1: raise
            time.sleep(5)
    return '', {}

frames = sorted(FR.glob('frame_*.jpg'), key=lambda p: float(p.stem.split('_')[1][:-1]))
print(f"frames: {[f.name for f in frames]}")
msgs = []
for i, f in enumerate(frames):
    msgs.append({"role": "user", "content": [
        {"type": "image_url", "image_url": {"url": "data:image/jpeg;base64," + b64(f)}},
        {"type": "text", "text": f"(帧{i+1}/5, 时间点 {f.stem.split('_')[1]})"}]})
msgs.append({"role": "user", "content": OBS})

t0 = time.time()
obs, u1 = call(msgs)
print(f"=== 观察 {time.time()-t0:.0f}s, input_tokens={u1.get('input_tokens')} ===")
print(obs)
print()

t1 = time.time()
prompt = SUM_TMPL.replace('<OBS>', obs).replace('{series}', SERIES).replace('{title}', TITLE)
# 保险: 处理剩余的 { } 占位 (避免 format 冲突, 已手动替换)
stxt, u2 = call([{"role": "user", "content": prompt}], n=1200)
print(f"=== 综合 {time.time()-t1:.0f}s ===")
print(stxt)

m = re.search(r'\{.*\}', stxt, re.S)
parsed = json.loads(m.group(0)) if m else None
OUT.write_text(json.dumps({"video": str(FR), "title": TITLE, "obs": obs,
    "report": parsed, "raw": stxt, "elapsed_obs_s": round(time.time()-t0,1)},
    ensure_ascii=False, indent=2))
print(f"\nsaved: {OUT}")
