Compare commits

..

1 Commits

Author SHA1 Message Date
xiaoxia 93b3e896c8 fix(viral-video): 编导脚本生成timeout 90→150s + 修ai_client logger占位符bug
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Worker 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
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m40s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 37s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m31s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 4m9s
AI Code Review / AI Code Review (pull_request) Successful in 4m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 4m39s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 4m46s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 46s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 6m22s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 7m16s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 9m39s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m46s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 18m32s
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 Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 2s
P0根因:用户09:57反馈『生成不了文案』,排查确认:
1. #2215的call_llm→chat_completion修复本身正确,凌晨/上午日志均验证意图解析HTTP 200成功(33s返回1505 tokens)
2. 编导脚本fast tmo=90s超时(#2183沿用至今,非#2215引入):doubao-seed-2-1-pro输出完整分镜脚本token量大,高峰期read timeout 90.1s,重试期间被watchtower容器重启打断
3. ai_client.py:287 logger.info格式串6个占位符只传5个参数(缺_req_timeout),每次chat_completion成功都打TypeError假错误栈,干扰排查

修复:
- 编导脚本fast tmo 90→150s,pro 120→150s,支持ENV VIRAL_VIDEO_SCRIPT_FAST_TIMEOUT/VIRAL_VIDEO_SCRIPT_PRO_TIMEOUT覆盖
- 补ai_client.py logger.info缺失的_req_timeout参数

文件变更:
- apps/worker/worker_app/tasks/viral_video.py: 编导脚本超时上调
- packages/shared/ai_client.py: logger占位符补参
2026-10-06 10:15:27 +08:00
5 changed files with 42 additions and 179 deletions
File diff suppressed because one or more lines are too long
-3
View File
@@ -341,9 +341,6 @@ def confirm_copy(
raise HTTPException(status_code=403, detail="无权操作此任务")
if job.status != ViralVideoStatus.COPY_GENERATED:
raise HTTPException(status_code=409, detail=f"任务当前状态 {job.status} 不能确认文案(需 copy_generated)")
# #2218: 额外校验 copy_result 完整性,防止孤儿/脏数据进入渲染
if not isinstance(job.copy_result, dict) or not job.copy_result:
raise HTTPException(status_code=409, detail="文案数据缺失,请先点击「生成文案」")
# 积分预扣(已扣过/重试任务跳过)
from app.config import settings as _settings
+12 -29
View File
@@ -1679,7 +1679,7 @@ def run_viral_video_generate_copy(self: Task, job_id: str) -> dict:
image_analysis = job.image_analysis or {"products": []}
intent_result = _step_intent_parsing(job, image_analysis)
job.intent_result = intent_result
# #2218: 不在意图解析后单独落库,等 copy_result 生成后与 mark_copy_generated 一起原子写入
_save_job(repo, job, session)
_emit_progress(job_id, ViralVideoStage.INTENT_PARSING, 35.0, "意图解析完成")
# 阶段:编导脚本生成(核心耗时环节,已用快模型)
@@ -1737,8 +1737,7 @@ def run_viral_video_generate_copy(self: Task, job_id: str) -> dict:
except Retry:
raise
except Exception as e:
logger.error("[爆款视频][阶段2] 异常 job_id=%s: %s", job_id, e, exc_info=True)
# #2218: 阶段2任何异常都标记为 failed(由 _mark_failed_and_notify 处理),前端提示重试
logger.error("[爆款视频][阶段2] 异常: %s", e, exc_info=True)
_mark_failed_and_notify(job_id, session, None, None, str(e), ViralVideoStage.SCRIPT_GENERATION)
return {"ok": False, "job_id": job_id, "error": str(e)}
finally:
@@ -1897,26 +1896,16 @@ def _run_render_pipeline(job_id: str, session, repo, job) -> dict:
阶段2 generate-copy 已把 LLM 深度审核后置,这里在 TTS 前做最终审核(不通过则自动重写1次)。
所有阶段通过 _set_stage 持久化 current_stage/phase_message。
"""
image_analysis = job.image_analysis or {"products": []}
# #2218: render 流程严禁补生成意图+编导脚本。copy_result 必须由 generate-copy 提前准备好;
# 若缺失说明 generate-copy 未完成或数据丢失,直接报错让用户重新点「生成文案」。
# 如果没有 copy_result(旧数据/失败重试),现场补生成(意图+脚本,不走 LLM 审核,出片前会统一做)
copy_result = job.copy_result
_copy_src = "db"
if not isinstance(copy_result, dict) or not copy_result:
logger.error(
"[爆款视频][阶段3] copy_result 为空或无效,无法进入渲染流程。job_id=%s status=%s intent_len=%d,请重新触发「生成文案」",
job_id,
job.status,
len((job.intent_result or {}) if isinstance(job.intent_result, dict) else {}),
)
raise ValueError("文案数据缺失,请先点击「生成文案」完成文案生成后再生成视频")
logger.info(
"[爆款视频][阶段3] 进入渲染流程 job_id=%s copy_result_shots=%d copy_result_len=%d source=%s",
job_id,
len((copy_result.get("shots") or [])),
len(str(copy_result)),
_copy_src,
)
_set_stage(job, repo, session, ViralVideoStage.SCRIPT_GENERATION, "正在补生成编导脚本...")
intent = job.intent_result or _step_intent_parsing(job, image_analysis)
copy_result = _step_script_generation(job, intent, image_analysis)
job.mark_copy_generated(copy_result)
_save_job(repo, job, session)
# 出片前 LLM 深度合规审核(#2134 问题7:审核从阶段2后置到这里,不阻塞前端预览脚本)
_set_stage(job, repo, session, ViralVideoStage.REVIEW, "正在进行出片前合规审核...")
@@ -1929,18 +1918,12 @@ def _run_render_pipeline(job_id: str, session, repo, job) -> dict:
if isinstance(rewritten, dict) and rewritten:
copy_result = rewritten
else:
# #2218: 审核重写失败不再从意图解析重跑,直接报错让用户重新生成文案
logger.error(
"[爆款视频][阶段3] 合规审核未通过且自动重写失败 job_id=%s,终止渲染",
job_id,
)
raise ValueError("文案合规审核未通过,请修改文案后重试或重新生成文案")
intent = job.intent_result or _step_intent_parsing(job, image_analysis)
copy_result = _step_script_generation(job, intent, image_analysis)
_step_review(job, copy_result)
job.copy_result = copy_result
job.generated_copy_text = copy_result.get("voiceover_script", "") or ""
_save_job(repo, job, session)
except ValueError:
# #2218: 审核未通过/文案缺失的业务异常,不继续出片,向上抛出
raise
except Exception as e:
logger.warning("[爆款视频][阶段3] 合规审核异常,继续出片: %s", e)
_emit_progress(job_id, ViralVideoStage.REVIEW, 70.0, "合规审核完成")
@@ -420,7 +420,7 @@ def _assemble_v4(idx: int, fj: dict, ocr_texts: list[str]) -> dict[str, Any]:
# 人物类
if vtype == "person" or has_person:
# 取第一个人物信息(v5 schema人物信息在顶层)
# 取第一个人物信息(v4 schema人物信息在顶层)
person_info = fj
# 兼容people嵌套
ppl = fj.get("people")
@@ -429,75 +429,58 @@ def _assemble_v4(idx: int, fj: dict, ocr_texts: list[str]) -> dict[str, Any]:
has_person = True
portrait_prompt = _build_portrait_prompt_from_v4(person_info)
outfit_style = person_info.get("outfit_style") or ""
upper = person_info.get("upper_wear") or ""
lower = person_info.get("lower_wear") or ""
dress = person_info.get("dress_wear") or ""
outer = person_info.get("outerwear") or ""
if dress:
name = str(dress)[:25]
elif outer and upper:
name = f"{outer}+{upper}"[:30]
elif upper:
name = (str(upper) + (f"+{lower}" if lower else ""))[:30]
name = person_info.get("upper_wear") or "人物穿搭"
if "连衣裙" in name:
pass
else:
name = "人物穿搭"
lower = person_info.get("lower_wear") or ""
if lower:
name = f"{name}+{lower}"
brand = "无法判断"
category = "人物穿搭"
# appearance: 外套+上衣+下装/裙+鞋+包+发型+妆容
app_parts = []
for k in ("outerwear", "upper_wear", "lower_wear", "dress_wear", "shoes", "bag", "hairstyle", "makeup"):
category = "服饰"
outfit_parts = []
for k in ("upper_wear", "lower_wear", "dress_color", "upper_color", "lower_color", "outfit_style"):
v = person_info.get(k)
if v and v not in ("null", None, "无明显妆容"):
app_parts.append(str(v))
appearance = ";".join(app_parts) if app_parts else "人像穿搭整体造型"
# key_features: 服装+配饰+拍摄信息
if v and v not in ("null", None):
outfit_parts.append(str(v))
appearance = "、".join(outfit_parts) if outfit_parts else "人像穿搭整体造型"
# key_features: 穿搭特征+配饰
kf = []
for k in (
"outfit_style",
"upper_wear",
"lower_wear",
"dress_wear",
"outerwear",
"shoes",
"bag",
"upper_color",
"lower_color",
"hairstyle",
"expression",
"pose",
"outfit_style",
):
v = person_info.get(k)
if v and v not in ("null", None, "无法判断"):
kf.append(str(v))
acc = person_info.get("accessories") or []
if isinstance(acc, list):
for a in acc:
if a and str(a) not in kf:
kf.append(str(a))
elif isinstance(acc, str) and acc:
kf.append(acc)
for k in ("shot_type", "camera_angle", "lighting", "atmosphere"):
v = person_info.get(k)
if v and v not in ("null", None):
kf.append(str(v))
kf.extend(str(a) for a in acc if a)
if text_on_package:
kf.append(f"文字:{'/'.join(text_on_package[:3])}")
kf = kf[:8] or ["无法判断"]
summary = (outfit_style + " " if outfit_style and outfit_style not in name else "") + name[:25]
if not summary.strip():
kf.append(f"画面文字: {'/'.join(text_on_package[:3])}")
kf = kf[:6] or ["无法判断"]
summary = (person_info.get("outfit_style") or "") + (person_info.get("upper_wear") or "穿搭")
if not summary or summary == "穿搭":
summary = "人物穿搭"
return {
"name": name[:30],
"brand": brand,
"category": category,
"appearance": appearance[:400],
"appearance": appearance,
"packaging": "人物形象无包装",
"text_on_package": text_on_package,
"key_features": kf,
"scene": scene,
"mood": mood,
"portrait_prompt": portrait_prompt[:300],
"summary": summary[:50],
"_source": "v2_fast_json_v5",
"portrait_prompt": portrait_prompt,
"summary": summary[:40],
"_source": "v2_fast_json_v4",
}
# 商品类
+4 -4
View File
@@ -137,18 +137,18 @@ V4_PERSON: dict[str, Any] = {
def test_assemble_v4_person() -> None:
r = assembler.assemble_result(0, V4_PERSON, [])
assert REQUIRED_KEYS <= set(r.keys())
assert r["category"] == "人物穿搭"
assert r["_source"] == "v2_fast_json_v5"
assert r["category"] == "服饰"
assert "T恤" in r["name"]
assert "阔腿裤" in r["name"]
assert "年轻女性" in r["portrait_prompt"]
assert "项链" in r["portrait_prompt"]
assert isinstance(r["key_features"], list) and len(r["key_features"]) <= 8
assert isinstance(r["key_features"], list) and len(r["key_features"]) <= 6
def test_assemble_v4_person_people_nested() -> None:
fj = {"type": "person", "people": {**V4_PERSON, "has_person": True}}
r = assembler.assemble_result(0, fj, [])
assert r["category"] == "人物穿搭"
assert r["category"] == "服饰"
assert "年轻女性" in r["portrait_prompt"]