fix(viral-video): P0-分镜卡片错误显示硬编码MOCK(实木餐桌)+流式XML标签裸显
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 7s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 7s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m50s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m49s
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 / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m16s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m3s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 45s
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
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 43s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m30s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m13s
AI Code Review / AI Code Review (pull_request) Successful in 6m57s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 4m20s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 6m10s
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
CI/CD Pipeline / Validate - Security (pull_request) Successful in 13m12s
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 / CI Gate (pull_request) Failing after 4s
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

问题1:流式输出显示<scrpit><clip><voiceover_script>等XML标签
根因:script_delta事件推送LLM原始XML输出,前端<pre>直接裸渲染,无标签过滤
修复:流式渲染时用正则剥离XML标签,只显示可读文本(copy_generated后仍用解析后的结构化卡片)

问题2:养生馆图片生成后卡片显示'实木餐桌/北美黑胡桃木餐桌',与图片内容完全无关
根因:renderCopyResult里isNewStructure判定过严——要求shots.length≥2+scene_and_lighting非空
+total_duration非空三个条件同时满足才用真实数据,否则fallback到硬编码MOCK_STORYBOARD
(MOCK_STORYBOARD写死的是'实木餐桌场景化种草')。后端_script_from_xml虽然有默认值,但
LLM输出不完整/解析异常时只要任何一个条件不满足就会回退到MOCK,用户看到与上传图片无关的餐桌文案
修复:
- 移除MOCK_STORYBOARD作为生产兜底(仅作开发示例,不能在真实用户场景出现)
- isNewStructure判定放宽:只要shots≥1或有voiceover_script就渲染真实数据
- 缺失字段用默认值补齐(scene_and_lighting默认'基于产品场景的真实画面',duration从shots时间轴推算)
- 新增durationForShots() helper从shots.time_range推算总时长

问题3:代码核查_step_script_generation入参链路——VLM summary_markdown通过_build_images_summary透传,
marketing_purpose/industry/duration/ratio/tone/target_customer/persona/viral_structure/language/
user_copy_text/style_guide全部正确传入user prompt,参数链路无遗漏,不需要修复
This commit is contained in:
CI Bot
2026-10-12 13:35:34 +08:00
parent 312210c3d5
commit ebf841901f
@@ -344,6 +344,16 @@ interface Storyboard {
}
/** 兼容旧 copy_result(final_copy/title/scenes)→ 新 Storyboard 结构 */
function durationForShots(shots: ShotScript[] | undefined): number {
if (!shots || shots.length === 0) return 15
let maxEnd = 0
for (const s of shots) {
const m = (s.time_range || "").match(/(\d+)-(\d+)秒?/)
if (m) maxEnd = Math.max(maxEnd, parseInt(m[2], 10))
}
return maxEnd > 0 ? maxEnd : 15
}
function copyResultToStoryboard(cr: CopyResult | null | undefined): Storyboard | null {
if (!cr) return null
// 新结构:含 shots/overview 字段直接用
@@ -1527,7 +1537,7 @@ const ViralVideoPage: React.FC = () => {
</div>
<div className="vv-sb-doc vv-streaming-doc">
<pre className="vv-streaming-pre">
{task.scriptStreamText}
{task.scriptStreamText.replace(/<\/?[a-zA-Z_][\w:-]*(?:\s[^>]*)?>/g, "").replace(/\s{3,}/g, "\n\n").trim()}
<span className="vv-streaming-caret" aria-hidden="true" />
</pre>
</div>
@@ -1537,16 +1547,41 @@ const ViralVideoPage: React.FC = () => {
</div>
)
}
// 后端新结构尚未就绪时,用 MOCK_STORYBOARD 做预览展示;新结构到了之后优先用真实数据
// P0修复:真实分镜数据渲染。移除MOCK_STORYBOARD硬编码兜底——旧逻辑isNewStructure判定过严
// (shots≥2 + scene_and_lighting非空 + duration非空)时才用真实数据,否则回退到写死的"实木餐桌"
// 导致任何字段缺失时用户看到与上传图片完全无关的MOCK卡片。改为:只要有shots或voiceover就用真实数据,
// 缺失字段用默认值补齐,永远不回退到MOCK(MOCK仅在开发预览时使用,生产绝不能出现)。
const sb: Storyboard | null = (() => {
if (!task.storyboard) return null
const isNewStructure =
task.storyboard.shots.length >= 2 &&
!!task.storyboard.scene_and_lighting &&
task.storyboard.overview.total_duration.length > 0
if (isNewStructure) return task.storyboard
// 后端还在旧结构:兜底用 mock 展示分镜卡片效果
return MOCK_STORYBOARD
const s = task.storyboard
// 有任何真实内容(shots≥1 或 有口播文案)就渲染真实数据
const hasRealContent =
(Array.isArray(s.shots) && s.shots.length >= 1) ||
(s.voiceover_script && s.voiceover_script.trim().length > 0)
if (!hasRealContent) return null
// 补齐可能缺失的字段,避免渲染崩溃
return {
...s,
overview: {
theme: s.overview?.theme || "AI 分镜脚本",
total_duration: s.overview?.total_duration || `${durationForShots(s.shots)}s`,
aspect_ratio: s.overview?.aspect_ratio || "9:16",
},
scene_and_lighting: s.scene_and_lighting || "基于产品场景的真实画面",
shots: (s.shots && s.shots.length > 0 ? s.shots : [{
time_range: "0-15s",
shot_type_angle_movement: "",
scene_and_dialogue: s.voiceover_script || "",
action_details: "",
audio_bgm: "轻快BGM",
transition: "",
reference_image_index: 0,
}]),
hard_constraints: Array.isArray(s.hard_constraints) ? s.hard_constraints : [],
negative_prompts: Array.isArray(s.negative_prompts) ? s.negative_prompts : [],
voiceover_script: s.voiceover_script || "",
copy_display_markdown: s.copy_display_markdown || "",
}
})()
if (
!sb &&