diff --git a/apps/worker/worker_app/tasks/viral_video.py b/apps/worker/worker_app/tasks/viral_video.py
index 8097c73bf..d0395d9c7 100644
--- a/apps/worker/worker_app/tasks/viral_video.py
+++ b/apps/worker/worker_app/tasks/viral_video.py
@@ -404,6 +404,50 @@ 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 重构后人物信息放在顶层 ,
+ # 不再是 的 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 解析: 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 解析标签异常: %s,回退无人像", idx, _pe)
for p in product_nodes:
a = p["attrs"]
text_on_pkg = a.get("text_on_package", "")
@@ -419,6 +463,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),否则用顶层 解析结果
+ _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 +477,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 标签但有 也要能取到人物描述(兜底)
+ 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:
diff --git a/packages/application/viral_video/prompts.py b/packages/application/viral_video/prompts.py
index 51a19151d..9307abe17 100644
--- a/packages/application/viral_video/prompts.py
+++ b/packages/application/viral_video/prompts.py
@@ -50,7 +50,7 @@ _IMAGE_ANALYSIS_SYSTEM = f"""你是电商商品视觉分析师,负责从商品
请严格按下面的标签格式输出,标签名一个都不能改,不要输出任何解释,不要用代码块:
下面每个产品用一个 标签,属性 name 是产品名、features 是外观特征、position 是 main 或 secondary、image_index 是第几张图(从0开始)。
下面每个主要颜色用一个 标签,属性 hex 是色值、name 是颜色名、coverage 是占比小数。
- 用一个标签,属性 has_person、count、gender、age_range、pose、expression 分别描述人物情况。
+ 用一个标签,属性 has_person、count、gender、age_range、hair(发型发色)、skin_tone(肤色)、face_shape(脸型)、outfit(穿着)、pose(姿态)、expression(表情)分别描述人物外貌。有人物时属性尽量具体(如hair="黑色长直发"、outfit="白色衬衫"),无人像时除has_person=false外其他填"无法判断"。
标签写画面整体情绪氛围。
下面每处可见文字用一个 标签,属性 text 是文字内容、position 是位置。
标签写场景描述。
@@ -73,7 +73,7 @@ _IMAGE_ANALYSIS_EXAMPLE = """
-
+
干净、实用