Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93b3e896c8 |
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
# 商品类
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user