fix(#2174 P0): 信任链从i2i改为t2i文生图——方舟实测验证通过
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging API Image (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 / 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 / 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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m19s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m17s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m11s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m16s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m36s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m43s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 5m56s
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled

#2170/#2172的信任链用Seedream图生图(i2i,传reference_images=用户真人照),
实测产物不被Seedance信任,仍被400 portrait_intercept拦截。

方舟文档+实测确认:信任的是Seedream文生图(t2i)产物——纯prompt描述人物,
不传reference_images,产物是纯模型生成人像,同账号30天内可直接传Seedance。

改动:
- VLM prompt新增portrait_prompt字段(60-100字人物外貌描述)
- preheat_trust_chain签名从portrait_urls改为portrait_descriptions,t2i模式不传reference_images
- video_generation移除现场跑i2i逻辑,仅使用预热好的t2i结果;预热失败走#2166自动降级t2v
- 预热时机从VLM前改为VLM后(需要VLM输出的portrait_prompt),仍与文案阶段并行
- 单测适配:TestTrustChain全部改为pre_trusted_images路径验证

实测验证(充值后):
- Seedream flash t2i ~15s出图,人像prompt中英文混合写实风格
- Seedance 2.5 i2v用t2i产物URL → 成功出5s视频(120s)
- Seedance 2.0-mini i2v用t2i产物URL → 成功出5s视频
- 对照组直传用户真人照片 → 400 portrait_intercept(预期)
This commit is contained in:
xiaoxia
2026-10-04 15:31:38 +08:00
parent 371d1b9daf
commit 0e78f175fe
4 changed files with 106 additions and 119 deletions
+37 -18
View File
@@ -92,13 +92,20 @@ def _save_job(repo, job, session):
session.commit()
def _start_trust_chain_preheat(job_id: str, portrait_urls: list[str]) -> None:
"""#2172 后台启动信任链预热(Seedream AI 化人像),不阻塞调用方。
def _start_trust_chain_preheat(job_id: str, portrait_descriptions: list[str]) -> None:
"""#2172/#2174 后台启动信任链预热(Seedream t2i 文生图人像),不阻塞调用方。
#2174 重要:改为 t2i 文生图模式——用 VLM 分析出的人物外貌描述做 prompt,不传 reference_images,
产物是方舟信任模型输出,Seedance 直接放行不触发肖像审核。
i2i(传用户照片做 reference)产物不被信任,实测仍被 400 portrait_intercept 拦截。
预热成功后把结果写入 job.pre_trusted_images,阶段3 渲染直接使用,省掉串行等待。
预热失败静默(pre_trusted_images 保持 None),阶段3 会现场跑信任链兜底。
预热失败静默(pre_trusted_images 保持 None),阶段3 会走 #2166 自动降级纯 t2v。
"""
if not portrait_urls:
# 过滤有效描述:非空且不是"无人像"
_valid = [d for d in (portrait_descriptions or []) if d and isinstance(d, str) and "无人像" not in d and len(d) >= 10]
if not _valid:
logger.info("[trust-chain][preheat] 无有效人物描述(可能是纯商品图),跳过预热 job=%s", job_id)
return
# 判断是否是 doubao provider(DashScope/Wan 不需要信任链)
try:
@@ -122,16 +129,16 @@ def _start_trust_chain_preheat(job_id: str, portrait_urls: list[str]) -> None:
except Exception:
pass
logger.info("[trust-chain][preheat] 后台预热启动 job=%s n=%d", job_id, len(portrait_urls))
result = preheat_trust_chain(portrait_urls, timeout=120)
if result and len(result) == len(portrait_urls):
logger.info("[trust-chain][preheat] 后台t2i预热启动 job=%s n=%d", job_id, len(_valid))
result = preheat_trust_chain(_valid, timeout=120)
if result and len(result) >= 1:
sess2, repo2, job2 = _get_repo_and_job(job_id)
try:
job2.pre_trusted_images = result
repo2.update(job2)
sess2.commit()
logger.info(
"[trust-chain][preheat] 预热完成并持久化 job=%s n=%d",
"[trust-chain][preheat] t2i预热完成并持久化 job=%s n=%d",
job_id,
len(result),
)
@@ -318,15 +325,17 @@ _IMAGE_ANALYSIS_SYSTEM_PROMPT = """你是电商商品视觉分析师,从商品
"3-5 条图片中能看到的外观/视觉特征(如『红色瓶盖白色瓶身』『鸡头图案 Logo』等)"
],
"scene": "图片场景(如白底棚拍/浴室实拍/桌面静物/手持实拍等)",
"portrait_prompt": "如果图片中有清晰人物面部,用60-100字中文描述该人物外貌(性别、年龄段、发型/发色、肤色、脸型、五官特征、当前穿着、表情姿态),用于AI生图参考;没有人物或看不清面部填「无人像」",
"summary": "100-180字中文导购描述,连贯自然段落,像电商详情页介绍,前端直接展示,必须提到品牌/品名/核心外观特征,不能写『无法判断』"
}"""
_IMAGE_ANALYSIS_USER_PROMPT = """请分析这张商品图片,输出严格 JSON。重点:
1. name/brand/text_on_package 从图片包装 OCR 读取,不编造;
2. appearance/packaging 各写 50-100 字,要具体;
3. summary 必须是 100-180 字连贯中文段落,说清商品是什么、长什么样、适合谁用,不要写「无法判断」;
4. 非产品图时 category 填「非产品图」,name 填实际看到的内容;
5. 看不清的字段填「无法判断」。"""
3. portrait_prompt:有人物时详细描述外貌(性别/年龄/发型/肤色/穿着/表情)用于AI人像生成参考,无人像填「无人像」;
4. summary 必须是 100-180 字连贯中文段落,说清商品是什么、长什么样、适合谁用,不要写「无法判断」;
5. 非产品图时 category 填「非产品图」,name 填实际看到的内容;
6. 看不清的字段填「无法判断」。"""
def _vision_fallback(idx: int, reason: str, extra: dict | None = None) -> dict:
@@ -338,6 +347,7 @@ def _vision_fallback(idx: int, reason: str, extra: dict | None = None) -> dict:
"text_on_package": [],
"key_features": [],
"scene": "通用",
"portrait_prompt": "无人像",
"summary": "",
"_source": reason,
}
@@ -414,6 +424,7 @@ def _analyze_single_image(
raw.setdefault("text_on_package", [])
raw.setdefault("key_features", [])
raw.setdefault("scene", "通用")
raw.setdefault("portrait_prompt", "无人像")
raw.setdefault("summary", "")
if not isinstance(raw.get("text_on_package"), list):
raw["text_on_package"] = []
@@ -1357,15 +1368,23 @@ def run_viral_video_pipeline(self: Task, job_id: str) -> dict:
_hb_stop, _hb_thread = _start_heartbeat_thread(job_id)
_set_stage(job, repo, session, ViralVideoStage.IMAGE_ANALYSIS, "正在分析商品特征...")
# #2172: 信任链预热与 VLM 分析并行启动(不阻塞)
if job.images:
try:
_start_trust_chain_preheat(job.id, list(job.images))
except Exception as _e:
logger.warning("[爆款视频][阶段1] 启动信任链预热失败: %s", _e)
image_analysis = _step_image_analysis(job)
job.image_analysis = image_analysis
# #2174: VLM完成后立即启动信任链t2i预热(从VLM结果提取人物描述),与文案阶段并行
if job.images:
try:
_products = (image_analysis or {}).get("products", []) or []
_portrait_descs = [
(p.get("portrait_prompt") or "无人像") for p in _products
] if _products else []
# 兼容单图结果格式(非products列表)
if not _portrait_descs and isinstance(image_analysis, dict):
_pp = image_analysis.get("portrait_prompt") or "无人像"
if _pp and _pp != "无人像":
_portrait_descs = [_pp]
_start_trust_chain_preheat(job.id, _portrait_descs)
except Exception as _e:
logger.warning("[爆款视频][阶段1] 启动信任链t2i预热失败: %s", _e)
_save_job(repo, job, session)
_emit_progress(job_id, ViralVideoStage.IMAGE_ANALYSIS, 15.0, "图片分析完成", {"result": image_analysis})