141 lines
5.3 KiB
Python
141 lines
5.3 KiB
Python
|
|
#!/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": <bool>, "content_ok": <bool>, "has_text": <bool>, "is_photorealistic": <bool>, "is_easel": <bool>, "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())
|