#!/usr/bin/env python3 """QC: PROP-002牌匾 + PROP-003广告牌 R2候选图 · 闸门1""" import sys, os, json, base64, time, subprocess from pathlib import Path sys.path.insert(0, os.path.dirname(__file__)) from secrets_loader import secret, endpoint AK = secret("SC-004") URL = endpoint("EPT-002") ROOT = Path(__file__).resolve().parents[1] # 候选图路径 CANDIDATES = { "PROP-002": { "name": "天道宗无字牌匾", "spec": "深棕色木质无字牌匾,长方形横版,边缘古朴雕花装饰,3D动漫卡通渲染风格,纯白背景,无文字", "reject": ["写实", "照片感", "真实木纹", "产品摄影", "文字", "汉字", "字母"], "dir": ROOT / "assets/candidates/D157-PROP-002-R2/PROP-002-Tiandao-Plaque", "files": [f"PROP-002-R2-candidate-{i:02d}-1080x1920.jpg" for i in range(1, 5)] }, "PROP-003": { "name": "免费招徒广告牌", "spec": "木制立式支架+米黄色空白宣纸底板,原木色支架朴素稳固,宣纸干净无字墨迹,修仙世界摊位宣传木板,3D动漫卡通渲染,纯白背景", "reject": ["写实", "照片感", "画架", "easel", "美术画架", "真实木纹", "产品图", "文字", "墨迹"], "dir": ROOT / "assets/candidates/D157-PROP-003-R2/PROP-003-Recruit-Board", "files": [f"PROP-003-R2-candidate-{i:02d}-1080x1920.jpg" for i in range(1, 5)] } } def encode_image(path): with open(path, "rb") as f: return base64.b64encode(f.read()).decode() def qwen_vl_qc(img_b64, asset_name, spec, reject_words): """调用Qwen-VL质检单张图""" prompt = f"""你是一个动态漫资产生成质检员。请严格检查这张图片。 资产名称:{asset_name} 规范要求:{spec} 请逐项检查并打分(0-100): 1. 风格匹配度(是否为3D动漫卡通渲染风格,非写实/照片感) 2. 内容准确性(是否包含规范要求的所有元素) 3. 一票否决项检查(以下任意一项出现直接判不合格): - 是否有文字/汉字/字母? - 是否是写实/照片感风格? - 是否像产品摄影图? - 对于PROP-003:是否是画架(easel)样式而不是摊位木板? 输出格式(严格JSON): {{"score": <0-100>, "style_ok": , "content_ok": , "has_text": , "is_photorealistic": , "is_easel": , "issues": ["问题列表"], "verdict": "PASS/FAIL"}}""" payload = { "model": "qwen-vl-max-latest", "input": { "messages": [ {"role": "user", "content": [ {"image": f"data:image/jpeg;base64,{img_b64}"}, {"text": prompt} ]} ] }, "parameters": {"enable_thinking": False} } r = subprocess.run( ["curl", "-s", "-m", "60", "--noproxy", "*", "-X", "POST", URL, "-H", f"Authorization: Bearer {AK}", "-H", "Content-Type: application/json", "-d", json.dumps(payload)], capture_output=True, text=True, timeout=65 ) try: d = json.loads(r.stdout) content = d["output"]["choices"][0]["message"]["content"] # 提取JSON import re m = re.search(r'\{[^{}]*\}', content) if m: return json.loads(m.group()) return {"score": 0, "verdict": "FAIL", "issues": ["无法解析VL响应"], "raw": content[:200]} except Exception as e: return {"score": 0, "verdict": "FAIL", "issues": [f"API错误: {e}"]} def main(): if not AK: print("✗ SC-004密钥未找到"); return 1 results = {} for aid, cfg in CANDIDATES.items(): print(f"\n{'='*50}") print(f"QC: {aid} · {cfg['name']}") print(f"{'='*50}") results[aid] = [] for fname in cfg["files"]: fpath = cfg["dir"] / fname if not fpath.exists(): print(f" ✗ 文件不存在: {fname}") continue print(f"\n 检查: {fname} ...", end=" ", flush=True) img_b64 = encode_image(str(fpath)) qc = qwen_vl_qc(img_b64, cfg["name"], cfg["spec"], cfg["reject"]) v = qc.get("verdict", "?") s = qc.get("score", "?") issues = qc.get("issues", []) print(f"[{v}] score={s}") for iss in issues: print(f" ⚠️ {iss}") qc["_file"] = fname results[aid].append(qc) time.sleep(1) # 输出汇总 print(f"\n\n{'='*60}") print("QC 汇总报告") print(f"{'='*60}") for aid, qlist in results.items(): print(f"\n【{aid}】") for q in qlist: fn = q.get("_file", "?") v = q.get("verdict", "?") s = q.get("score", "?") ph = q.get("is_photorealistic", False) easel = q.get("is_easel", False) has_txt = q.get("has_text", False) flag = "" if ph: flag += " 写实" if easel: flag += " 画架" if has_txt: flag += " 有字" print(f" {fn}: {v} (score={s}){flag}") # 写入QC报告 report_path = ROOT / "assets/candidates/D157-PROP-QC-report.json" with open(report_path, "w") as f: json.dump(results, f, ensure_ascii=False, indent=2) print(f"\n📋 QC报告已写入: {report_path}") return 0 if __name__ == "__main__": sys.exit(main())