- CURRENT-STATE 更新至D158 - 生成脚本: char004/char004_r3/prop002/prop002_r3/prop003 全量 - QC脚本: qc_char004_r2/r3 + qc_prop_r2 - QC报告: CHAR-004 R2~R5 + PROP全量 - generation-meta: 所有候选批次元数据 - 候选图(JPG)保留本地不入库
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())
|