Files
ViGent2/Docs/DevLogs/Day21.md
Kevin Wong 3129d45b25 更新
2026-02-09 14:47:19 +08:00

20 KiB
Raw Blame History

🐛 缺陷修复:视频生成与持久化回归 (Day 21)

概述

本日修复 Day 20 优化后引入的 3 个回归缺陷Remotion 渲染崩溃容错、首页作品选择持久化、发布页作品选择持久化。


已完成修复

BUG-1: Remotion 渲染进程崩溃导致标题/字幕丢失

  • 现象: 视频生成后没有标题和字幕,回退到纯 FFmpeg 合成。
  • 根因: Remotion Node.js 进程在渲染完成100%)后以 SIGABRT (code -6) 退出Python 端将其视为失败。
  • 修复: remotion_service.py 在进程非零退出时,先检查输出文件是否存在且大小合理(>1KB若存在则视为成功。
  • 文件: backend/app/services/remotion_service.py
if process.returncode != 0:
    output_file = Path(output_path)
    if output_file.exists() and output_file.stat().st_size > 1024:
        logger.warning(
            f"Remotion process exited with code {process.returncode}, "
            f"but output file exists ({output_file.stat().st_size} bytes). Treating as success."
        )
        return output_path
    raise RuntimeError(...)

BUG-2: 首页历史作品选择刷新后不保持

  • 现象: 用户选择某个历史作品后刷新页面,总是回到第一个视频。
  • 根因: fetchGeneratedVideos() 在初始加载时无条件自动选中第一个视频,覆盖了 useHomePersistence 的恢复值。
  • 修复: fetchGeneratedVideos 增加 preferVideoId 参数,仅在明确指定时才自动选中;新增 "__latest__" 哨兵值用于生成完成后选中最新。
  • 文件: frontend/src/features/home/model/useGeneratedVideos.ts, frontend/src/features/home/model/useHomeController.ts
// 任务完成 → 自动选中最新
useEffect(() => {
  if (prevIsGenerating.current && !isGenerating) {
    if (currentTask?.status === "completed") {
      void fetchGeneratedVideos("__latest__");
    } else {
      void fetchGeneratedVideos();
    }
  }
  prevIsGenerating.current = isGenerating;
}, [isGenerating, currentTask, fetchGeneratedVideos]);

BUG-3: 发布页作品选择刷新后不保持(根因:签名 URL 不稳定)

  • 现象: 发布管理页选择视频后刷新,选择丢失(无任何视频被选中)。
  • 根因: 后端 /api/videos/generated 返回的 path 是 Supabase 签名 URL每次请求都会变化。发布页用 path 作为选择标识存入 localStorage刷新后新的 path 与保存值永远不匹配。首页不受影响是因为使用稳定的 video.id
  • 修复: 发布页全面改用 id(稳定标识)替代 path(签名 URL进行选择、持久化和比较。
  • 文件:
    • frontend/src/shared/types/publish.tsPublishVideo 新增 id 字段
    • frontend/src/features/publish/model/usePublishController.tsselectedVideo 存储 id,发布时根据 id 查找 path
    • frontend/src/features/publish/ui/PublishPage.tsxkey/onClick/选中比较改用 v.id
    • frontend/src/features/home/model/useHomeController.ts — 预取缓存加入 id 字段
// 类型定义新增 id
export interface PublishVideo {
    id: string;    // 稳定标识符
    name: string;
    path: string;  // 签名 URL仅用于播放/发布)
}

// 发布时根据 id 查找 path
const video = videos.find(v => v.id === selectedVideo);
await api.post('/api/publish', { video_path: video.path, ... });

涉及文件汇总

文件 变更
backend/app/services/remotion_service.py Remotion 崩溃容错
frontend/src/features/home/model/useGeneratedVideos.ts 首页视频选择不自动覆盖
frontend/src/features/home/model/useHomeController.ts 任务完成监听 + 预取缓存加 id
frontend/src/shared/types/publish.ts PublishVideo 新增 id 字段
frontend/src/features/publish/model/usePublishController.ts 选择/持久化/发布改用 id
frontend/src/features/publish/ui/PublishPage.tsx UI 选择比较改用 id

关键教训

签名 URL 不可作为持久化标识。Supabase Storage 的签名 URL 包含时间戳和签名参数,每次请求都不同。任何需要跨请求/跨刷新保持的标识,必须使用后端返回的稳定 id 字段。

重启要求

pm2 restart vigent2-backend    # Remotion 容错
npm run build && pm2 restart vigent2-frontend  # 前端持久化修复

🎨 浮动样式预览窗口优化 (Day 21)

概述

标题与字幕面板中的预览区域原本是内联折叠的,展开后调节下方滑块时看不到预览效果。改为 position: fixed 浮动窗口,固定在视口左上角,滚动页面时预览始终可见,边调边看。

已完成优化

1. 新建浮动预览组件 FloatingStylePreview.tsx

  • createPortal(jsx, document.body) 渲染到 body 层级,脱离面板 DOM 树
  • position: fixed + 左上角固定定位,滚动时不移动
  • z-index: 150(低于 VideoPreviewModal 的 200
  • 顶部标题栏 + X 关闭按钮ESC 键关闭
  • 桌面端固定宽度 280px移动端自适应最大 360px
  • previewScale = windowWidth / previewBaseWidth 自行计算缩放
  • maxHeight: calc(100dvh - 32px) 防止超出视口

2. 修改 TitleSubtitlePanel.tsx

  • 删除内联预览区域(ref={previewContainerRef} 整块 JSX
  • 条件渲染 <FloatingStylePreview />,按钮文本保持"预览样式"/"收起预览"
  • 移除 previewScalepreviewAspectRatiopreviewContainerRef props
  • 保留 previewBaseWidth/Height(浮动窗口需要原始尺寸计算 scale

3. 清理 useHomeController.ts

  • 移除 previewContainerWidth 状态
  • 移除 titlePreviewContainerRef ref
  • 移除 ResizeObserver useEffect浮动窗口自管尺寸不再需要

4. 简化 HomePage.tsx 传参

  • 移除 previewContainerWidthtitlePreviewContainerRef 解构
  • 移除 previewScalepreviewAspectRatiopreviewContainerRef prop 传递

5. 移动端适配

  • ScriptEditor.tsx:标题行改为 flex-wrap"AI生成标题标签"按钮不再溢出
  • 预览默认比例从 1280×720 (16:9) 改为 1080×1920 (9:16),符合抖音竖屏视频

涉及文件汇总

文件 变更
frontend/src/features/home/ui/FloatingStylePreview.tsx 新建 浮动预览组件
frontend/src/features/home/ui/TitleSubtitlePanel.tsx 移除内联预览,渲染浮动组件
frontend/src/features/home/model/useHomeController.ts 移除 preview 容器相关状态和 ResizeObserver
frontend/src/features/home/ui/HomePage.tsx 简化 props 传递,默认比例改 9:16
frontend/src/features/home/ui/ScriptEditor.tsx 移动端按钮换行适配

重启要求

npm run build && pm2 restart vigent2-frontend

🔧 多平台发布体系重构:用户隔离与抖音刷脸验证 (Day 21)

概述

重构发布系统的两大核心问题:① 多用户场景下 Cookie/会话缺乏隔离,② 抖音登录新增刷脸验证步骤无法处理。同时修复了平台配置混用和微信视频号发布流程问题。


一、平台配置独立化

问题

所有平台抖音、微信、B站、小红书共用 WEIXIN_* 配置,导致 User-Agent、Headless 模式等设置不匹配。

修复 — config.py

  • 新增 DOUYIN_* 独立配置项:DOUYIN_HEADLESS_MODEDOUYIN_USER_AGENTChrome/144DOUYIN_LOCALEDOUYIN_TIMEZONE_IDDOUYIN_CHROME_PATHDOUYIN_FORCE_SWIFTSHADER、调试开关等
  • 微信保持已有 WEIXIN_* 配置
  • B站/小红书使用通用默认值

修复 — qr_login_service.py 平台配置映射

# 之前:所有平台都用 WEIXIN 设置
# 之后:每个平台独立配置
PLATFORM_CONFIGS = {
    "douyin": { headless, user_agent, locale, timezone... },
    "weixin": { headless, user_agent, locale, timezone... },
    "bilibili": { 通用配置 },
    "xiaohongshu": { 通用配置 },
}

问题

多用户共享同一套 Cookie 文件,用户 A 的登录态可能被用户 B 覆盖。

修复 — publish_service.py

  • _get_cookies_dir(user_id)backend/user_data/{uuid}/cookies/
  • _get_cookie_path(user_id, platform) → 按用户+平台返回独立 Cookie 文件路径
  • _get_session_key(user_id, platform)"{user_id}_{platform}" 格式的会话 key
  • 登录/发布流程全链路传入 user_id,清理残留会话避免干扰

三、抖音刷脸验证二维码

问题

抖音扫码登录后可能弹出刷脸验证窗口,内含新的二维码需要用户再次扫描,前端无法感知和展示。

修复 — 后端 qr_login_service.py

  • 扩展 QR 选择器:支持跨 iframe 搜索二维码元素
  • 抖音 API 拦截:监听 check_qrconnect 响应,检测 redirect_url
  • 检测 "完成验证" / "请前往APP完成验证" 文案
  • 在验证弹窗内找到正方形二维码(排除头像),截图返回给前端
  • API 确认后直接导航到 redirect_url不重新加载 QR 页,避免销毁会话)

修复 — 后端 publish_service.py

  • get_login_session_status() 新增 face_verify_qr 字段返回
  • 登录成功且 Cookie 保存后自动清理会话

修复 — 前端

  • usePublishController.ts:新增 faceVerifyQr 状态,轮询时获取 face_verify_qr 字段
  • PublishPage.tsxQR 弹窗优先展示刷脸验证二维码,附提示文案
{faceVerifyQr ? (
  <>
    <Image src={`data:image/png;base64,${faceVerifyQr}`} />
    <p>需要身份验证,请用抖音APP扫描上方二维码完成刷脸验证</p>
  </>
) : /* 普通登录二维码 */ }

四、微信视频号发布流程优化

修复 — weixin_uploader.py

  • 添加 user_id 参数支持,发布截图目录隔离
  • 新增 post_create API 响应监听,精准判断发布成功
  • 发布结果判定URL 离开创建页 或 API 确认提交 → 视为成功
  • 标题/标签处理改为统一写入"视频描述"字段(不再单独填写 title/tags

涉及文件汇总

文件 变更
backend/app/core/config.py 新增 DOUYIN_* 独立配置项
backend/app/services/qr_login_service.py 平台配置拆分、刷脸验证二维码、跨 iframe 选择器
backend/app/services/publish_service.py 用户隔离 Cookie 管理、刷脸验证状态返回
backend/app/services/uploader/weixin_uploader.py user_id 支持、post_create API 监听、描述字段合并
frontend/src/features/publish/model/usePublishController.ts faceVerifyQr 状态
frontend/src/features/publish/ui/PublishPage.tsx 刷脸验证二维码展示

重启要求

pm2 restart vigent2-backend    # 发布服务 + QR登录
npm run build && pm2 restart vigent2-frontend  # 刷脸验证UI

🏗️ 架构优化:前端结构微调 + 后端模块分层 (Day 21)

概述

根据架构审计结果,完成前端目录规范化和后端核心模块的分层补全。

一、前端结构微调

1. ScriptExtractionModal 迁移

  • components/ScriptExtractionModal.tsxfeatures/home/ui/ScriptExtractionModal.tsx
  • 连带 components/script-extraction/ 目录一并迁移到 features/home/ui/script-extraction/
  • 更新 HomePage.tsx 的 import 路径

2. contexts/ 目录归并

  • src/contexts/AuthContext.tsxsrc/shared/contexts/AuthContext.tsx
  • src/contexts/TaskContext.tsxsrc/shared/contexts/TaskContext.tsx
  • 更新 6 处 importlayout.tsx, useHomeController.ts, usePublishController.ts, AccountSettingsDropdown.tsx, GlobalTaskIndicator.tsx
  • 删除空的 src/contexts/ 目录

3. 清理重构遗留空目录

  • 删除 src/lib/src/components/home/src/hooks/

二、后端模块分层补全

将 3 个 400+ 行的 router-only 模块拆分为 router.py + schemas.py + service.py

模块 改造前 改造后 router
materials/ 416 行 63 行
tools/ 417 行 33 行
ref_audios/ 421 行 71 行

业务逻辑全部提取到 service.py,数据模型定义在 schemas.pyrouter 只做参数校验 + 调用 service + 返回响应。

三、开发规范更新

BACKEND_DEV.md 第 8 节新增渐进原则:

  • 新模块必须包含 router.py + schemas.py + service.py
  • 改旧模块时顺手拆涉及的部分
  • 新代码高标准,旧代码逐步改

涉及文件汇总

文件 变更
frontend/src/features/home/ui/ScriptExtractionModal.tsx 从 components/ 迁入
frontend/src/features/home/ui/script-extraction/ 从 components/ 迁入
frontend/src/shared/contexts/AuthContext.tsx 从 contexts/ 迁入
frontend/src/shared/contexts/TaskContext.tsx 从 contexts/ 迁入
backend/app/modules/materials/schemas.py 新建
backend/app/modules/materials/service.py 新建
backend/app/modules/materials/router.py 精简为薄路由
backend/app/modules/tools/schemas.py 新建
backend/app/modules/tools/service.py 新建
backend/app/modules/tools/router.py 精简为薄路由
backend/app/modules/ref_audios/schemas.py 新建
backend/app/modules/ref_audios/service.py 新建
backend/app/modules/ref_audios/router.py 精简为薄路由
Docs/BACKEND_DEV.md 目录结构标注分层、新增渐进原则
Docs/BACKEND_README.md 目录结构标注分层
Docs/FRONTEND_DEV.md 更新目录结构contexts 迁移、ScriptExtractionModal 迁移)

重启要求

pm2 restart vigent2-backend
npm run build && pm2 restart vigent2-frontend

🎬 多素材视频生成(多机位效果)

概述

支持用户上传多个不同角度的自拍视频,生成视频时按句子自动切换素材,最终效果类似多机位拍摄。单素材时走原有流程,无额外开销。

核心架构

流水线变更

【单素材(不变)】
text → TTS → audio → LatentSync(1个素材+完整audio) → Whisper字幕 → Remotion → 成片

【多素材(新增)】
text → TTS → audio → Whisper字幕(提前) → 按素材数量均分时长(对齐字边界)
  → 对每段: 切分audio + LatentSync(素材[i]+音频片段[i])
  → FFmpeg拼接所有片段 → Remotion(完整字幕时间戳) → 成片

素材切换逻辑(均分方案)

  1. Whisper 对完整音频转录,得到字级别时间戳
  2. 按素材数量均分音频总时长total_duration / N
  3. 每个分割点对齐到最近的 Whisper 字边界,避免在字中间切分
  4. 首段 start 扩展为 0.0,末段 end 扩展为音频结尾,确保完整覆盖

设计决策:最初方案基于原始文案标点分句,但用户文案往往不含句号(只有逗号),导致只产生 1 段。改为均分方案后不依赖文案标点,对任何输入都能正确切分。


一、后端改动

1. backend/app/modules/videos/schemas.py

  • 新增 material_paths: Optional[List[str]] 字段
  • 保留 material_path: str 向后兼容

2. backend/app/modules/videos/workflow.py(核心改动)

新增函数

  • _split_equal(segments, material_paths): 按素材数量均分音频时长,对齐到最近的 Whisper 字边界

修改 process_video_generation()

  • is_multi = len(material_paths) > 1 判断走多素材/单素材分支
  • 多素材分支Whisper 提前 → 均分切分 → 音频切分 → 逐段 LatentSync → FFmpeg 拼接

3. backend/app/services/video_service.py

  • 新增 concat_videos(): FFmpeg concat demuxer (-c copy) 拼接视频片段
  • 新增 split_audio(): FFmpeg 按时间范围切分音频 (-ss + -t + -c copy)

4. backend/scripts/watchdog.py

  • 健康检查阈值从 3 次提高到 5 次(容忍期 2.5 分钟)
  • 新增重启后 120 秒冷却期,避免模型加载期间被误判为故障
  • 启动时给所有服务 60 秒初始冷却期

二、前端改动

1. 新增依赖

npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

2. frontend/src/features/home/model/useMaterials.ts

  • selectedMaterial: stringselectedMaterials: string[](多选)
  • 新增 toggleMaterial(id): 切换选中/取消至少保留1个
  • 新增 reorderMaterials(activeId, overId): 拖拽排序
  • 上传格式扩展:新增 .mkv/.webm/.flv/.wmv/.m4v/.ts/.mts

3. frontend/src/features/home/ui/MaterialSelector.tsx(重写)

  • 素材列表每行增加复选框 + 序号徽标(①②③)
  • 选中 ≥2 个时显示拖拽排序区(@dnd-kit SortableContext
  • 每个排序项:拖拽把手 + 序号 + 素材名 + 移除按钮
  • HTML input accept 改为 video/*

4. frontend/src/features/home/model/useHomeController.ts

  • 多素材 payloadmaterial_paths 数组 + material_path 向后兼容
  • enable_subtitles 硬编码为 true(移除开关)
  • 验证:至少选中 1 个素材

5. frontend/src/features/home/model/useHomePersistence.ts

  • 素材持久化改为 JSON 数组,向后兼容旧格式(单字符串)
  • 移除 enableSubtitles 持久化

6. frontend/src/features/home/ui/TitleSubtitlePanel.tsx

  • 移除"逐字高亮字幕"开关,字幕样式区始终显示

7. frontend/src/features/home/ui/HomePage.tsx

  • 更新 props 传递(selectedMaterials, toggleMaterial, reorderMaterials

三、Bug 修复记录

BUG-1: 多素材只使用第一个视频(基于标点的分句方案失败)

  • 现象: 选了 2 个素材但生成的视频只使用第 1 个,日志显示 Multi-material: 1 segments, 2 materials
  • 根因 v1: 最初通过正则 [。!?!?] 在 Whisper 输出中分句,但 Whisper 不输出标点。
  • 修复 v1: 改为用原始文案标点分句——但用户文案往往只含逗号(,),无句末标点(。!?),仍退化为 1 段。
  • 最终修复: 彻底放弃基于标点的分句方案,改为 _split_equal() 按素材数量均分音频时长,对齐到最近的 Whisper 字边界。不依赖任何标点符号,对所有文案均有效。

BUG-2: 口型对不上(音频时间偏移)

  • 根因: split_audio 用 Whisper 的 start/end 时间(如 0.117.21)切分音频,但 compose() 用完整原始音频0.0结尾)合成,导致时间偏移。
  • 修复: 强制首段 start=0.0,末段 end=音频实际时长,确保切分音频完整覆盖。

BUG-3: min_segment_sec 过度合并导致退化(已随方案切换移除)

  • 根因: 旧方案中 2 个句子第 2 句不足 3 秒时,最短时长检查合并为 1 段,多素材退化为单素材。
  • 状态: 均分方案不存在此问题,相关代码已移除。

涉及文件汇总

文件 变更类型 说明
backend/app/modules/videos/schemas.py 修改 新增 material_paths 字段
backend/app/modules/videos/workflow.py 修改 多素材流水线核心逻辑 + 3个 Bug 修复
backend/app/services/video_service.py 修改 新增 concat_videos / split_audio
backend/scripts/watchdog.py 修改 阈值优化 + 冷却期机制
frontend/package.json 修改 新增 @dnd-kit 依赖
frontend/src/features/home/model/useMaterials.ts 修改 多选 + 排序状态管理
frontend/src/features/home/ui/MaterialSelector.tsx 重写 多选复选框 + 拖拽排序 UI
frontend/src/features/home/model/useHomeController.ts 修改 多素材 payload + 移除字幕开关
frontend/src/features/home/model/useHomePersistence.ts 修改 JSON 数组持久化
frontend/src/features/home/ui/TitleSubtitlePanel.tsx 修改 移除字幕开关
frontend/src/features/home/ui/HomePage.tsx 修改 更新 props 传递

重启要求

pm2 restart vigent2-backend
npm run build && pm2 restart vigent2-frontend