Merge pull request #2184: fix xml people portrait_prompt trust-chain
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 24s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m1s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m31s
CI/CD Pipeline / Build Staging API Image (push) Successful in 3m40s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m5s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m15s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 6m19s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
AI Code Review / AI Code Review (pull_request) Successful in 7m16s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m54s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 7m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m37s
CI/CD Pipeline / Integration Tests (push) Successful in 9m35s
CI/CD Pipeline / Validate - Style (push) Successful in 10m9s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m30s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 11m21s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m52s
CI/CD Pipeline / Validate - Security (push) Successful in 16m10s
CI/CD Pipeline / Unit Tests (push) Successful in 17m38s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped

This commit was merged in pull request #2184.
This commit is contained in:
2026-10-04 23:49:36 +08:00
2 changed files with 77 additions and 3 deletions
+75 -1
View File
@@ -404,6 +404,60 @@ def _analyze_single_image(
product_nodes = [n for n in nodes if n["tag"] == "product"]
scene = xp.text_of(raw_text, "scene") or "通用"
mood = xp.text_of(raw_text, "mood") or ""
# #2184: #2177 XML 重构后人物信息放在顶层 <people has_person count gender age_range pose expression/>,
# 不再是 <product> 的 portrait_prompt 属性。需从顶层 people 标签提取并拼装 portrait_prompt。
portrait_prompt = "无人像"
try:
people_node = xp.find_first(raw_text, "people")
if people_node:
_pa = people_node.get("attrs") or {}
_has_person = xp.attr_bool(_pa.get("has_person"), False)
if _has_person:
_gender = _pa.get("gender", "无法判断") or "无法判断"
_age = _pa.get("age_range", "无法判断") or "无法判断"
_hair = _pa.get("hair", "无法判断") or "无法判断"
_skin = _pa.get("skin_tone", "无法判断") or "无法判断"
_face = _pa.get("face_shape", "无法判断") or "无法判断"
_outfit = _pa.get("outfit", "无法判断") or "无法判断"
_pose = _pa.get("pose", "无法判断") or "无法判断"
_expr = _pa.get("expression", "无法判断") or "无法判断"
_count = xp.attr_int(_pa.get("count"), 1)
_parts = []
if _gender != "无法判断":
_g = _gender + ("性" if not _gender.endswith("性") else "")
_parts.append(_g)
if _age != "无法判断":
_parts.append(_age)
_parts.append("人物")
if _hair != "无法判断":
_parts.append(_hair)
if _skin != "无法判断":
_parts.append(f"{_skin}肤色")
if _face != "无法判断":
_parts.append(f"{_face}脸型")
if _outfit != "无法判断":
_parts.append(f"身着{_outfit}")
if _pose != "无法判断":
_parts.append(f"姿态{_pose}")
if _expr != "无法判断":
_parts.append(f"表情{_expr}")
portrait_prompt = ",".join(_parts)
logger.info(
"[爆款视频] 图片 #%d 解析<people>: count=%d gender=%s age=%s hair=%s skin=%s face=%s outfit=%s pose=%s expr=%s → %s",
idx,
_count,
_gender,
_age,
_hair,
_skin,
_face,
_outfit,
_pose,
_expr,
portrait_prompt,
)
except Exception as _pe:
logger.warning("[爆款视频] 图片 #%d 解析<people>标签异常: %s,回退无人像", idx, _pe)
for p in product_nodes:
a = p["attrs"]
text_on_pkg = a.get("text_on_package", "")
@@ -419,6 +473,10 @@ def _analyze_single_image(
appearance = a.get("appearance", "") or "无法判断"
packaging = a.get("packaging", "") or "无法判断"
summary = a.get("summary", "") or f"{brand} {name}"
# 优先取 product 属性上的 portrait_prompt(兼容旧schema),否则用顶层 <people> 解析结果
_pp_from_attr = a.get("portrait_prompt", "")
if _pp_from_attr and _pp_from_attr != "无人像":
portrait_prompt = _pp_from_attr
return {
"name": name,
"brand": brand,
@@ -429,10 +487,26 @@ def _analyze_single_image(
"key_features": feat_list or [features] if features else ["无法判断"],
"scene": scene,
"mood": mood,
"portrait_prompt": a.get("portrait_prompt", "无人像"),
"portrait_prompt": portrait_prompt,
"summary": summary,
"_source": "xml",
}
# 没有 product 标签但有 <people has_person="true"> 也要能取到人物描述(兜底)
if portrait_prompt != "无人像":
return {
"name": "未识别",
"brand": "无法判断",
"category": "无法判断",
"appearance": "无法判断",
"packaging": "无法判断",
"text_on_package": [],
"key_features": ["无法判断"],
"scene": scene,
"mood": mood,
"portrait_prompt": portrait_prompt,
"summary": "未识别",
"_source": "xml_no_product",
}
return _vision_fallback(idx, "no_product_tag")
def _normalize(raw, source: str) -> dict:
+2 -2
View File
@@ -50,7 +50,7 @@ _IMAGE_ANALYSIS_SYSTEM = f"""你是电商商品视觉分析师,负责从商品
请严格按下面的标签格式输出,标签名一个都不能改,不要输出任何解释,不要用代码块:
<products> 下面每个产品用一个 <product> 标签,属性 name 是产品名、features 是外观特征、position 是 main 或 secondary、image_index 是第几张图(从0开始)。
<colors> 下面每个主要颜色用一个 <color> 标签,属性 hex 是色值、name 是颜色名、coverage 是占比小数。
<people> 用一个标签,属性 has_person、count、gender、age_range、pose、expression 分别描述人物情况。
<people> 用一个标签,属性 has_person、count、gender、age_range、hair(发型发色)、skin_tone(肤色)、face_shape(脸型)、outfit(穿着)、pose(姿态)、expression(表情)分别描述人物外貌。有人物时属性尽量具体(如hair="黑色长直发"、outfit="白色衬衫"),无人像时除has_person=false外其他填"无法判断"。
<mood> 标签写画面整体情绪氛围。
<visible_text> 下面每处可见文字用一个 <text_item> 标签,属性 text 是文字内容、position 是位置。
<scene> 标签写场景描述。
@@ -73,7 +73,7 @@ _IMAGE_ANALYSIS_EXAMPLE = """<products>
<color hex="#D32F2F" name="红色" coverage="0.4"/>
<color hex="#FFFFFF" name="白色" coverage="0.5"/>
</colors>
<people has_person="false" count="0" gender="无法判断" age_range="无法判断" pose="无法判断" expression="无法判断"/>
<people has_person="false" count="0" gender="无法判断" age_range="无法判断" hair="无法判断" skin_tone="无法判断" face_shape="无法判断" outfit="无法判断" pose="无法判断" expression="无法判断"/>
<mood>干净、实用</mood>
<visible_text>
<text_item text="多功能油污净" position="瓶身正面"/>