From 5357d9701214d4890747caffb34aa64d528af216 Mon Sep 17 00:00:00 2001 From: Kevin Wong Date: Wed, 4 Feb 2026 11:41:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Docs/DevLogs/Day16.md | 61 +- Docs/DevLogs/Day17.md | 154 ++ Docs/Doc_Rules.md | 79 +- Docs/FRONTEND_DEV.md | 69 +- Docs/FRONTEND_README.md | 17 +- Docs/implementation_plan.md | 86 +- Docs/task_complete.md | 22 +- README.md | 3 +- frontend/README.md | 95 -- frontend/src/app/page.tsx | 1335 ++++------------- frontend/src/app/publish/page.tsx | 195 ++- frontend/src/components/VideoPreviewModal.tsx | 49 +- frontend/src/components/home/BgmPanel.tsx | 137 ++ .../src/components/home/GenerateActionBar.tsx | 53 + frontend/src/components/home/HistoryList.tsx | 80 + frontend/src/components/home/HomeHeader.tsx | 30 + .../src/components/home/MaterialSelector.tsx | 168 +++ frontend/src/components/home/PreviewPanel.tsx | 74 + .../src/components/home/RefAudioPanel.tsx | 262 ++++ frontend/src/components/home/ScriptEditor.tsx | 66 + .../components/home/TitleSubtitlePanel.tsx | 309 ++++ .../src/components/home/VoiceSelector.tsx | 75 + frontend/src/lib/media.ts | 61 + 23 files changed, 2076 insertions(+), 1404 deletions(-) create mode 100644 Docs/DevLogs/Day17.md delete mode 100644 frontend/README.md create mode 100644 frontend/src/components/home/BgmPanel.tsx create mode 100644 frontend/src/components/home/GenerateActionBar.tsx create mode 100644 frontend/src/components/home/HistoryList.tsx create mode 100644 frontend/src/components/home/HomeHeader.tsx create mode 100644 frontend/src/components/home/MaterialSelector.tsx create mode 100644 frontend/src/components/home/PreviewPanel.tsx create mode 100644 frontend/src/components/home/RefAudioPanel.tsx create mode 100644 frontend/src/components/home/ScriptEditor.tsx create mode 100644 frontend/src/components/home/TitleSubtitlePanel.tsx create mode 100644 frontend/src/components/home/VoiceSelector.tsx create mode 100644 frontend/src/lib/media.ts diff --git a/Docs/DevLogs/Day16.md b/Docs/DevLogs/Day16.md index 999450d..fd4ed2a 100644 --- a/Docs/DevLogs/Day16.md +++ b/Docs/DevLogs/Day16.md @@ -1,5 +1,3 @@ ---- - ## 🔧 Qwen-TTS Flash Attention 优化 (10:00) ### 优化背景 @@ -18,8 +16,6 @@ pip install -U flash-attn --no-build-isolation - **显存占用**: 显著降低,消除 OOM 风险 - **代码变动**: 无代码变动,仅环境优化 (自动检测) ---- - ## 🛡️ 服务看门狗 Watchdog (10:30) ### 问题描述 @@ -53,67 +49,20 @@ if service["failures"] >= service['threshold']: --- -## 🎨 UI 交互体验优化 (15:30) +## 🎯 前端按钮图标统一 (16:40) -### 优化内容 -- 视频生成完成后,预览优先选中最新输出 -- 选择项持久化:素材 / 背景音乐 / 历史视频 -- 列表内滚动定位选中项,避免页面跳动 -- 刷新回顶部(首页 / 发布页) -- 背景音乐试听即选中并自动开启,音量滑块实时影响试听 +### 内容 +- 首页与发布页按钮图标统一替换为 Lucide SVG +- 交互按钮保持一致尺寸与对齐 ### 涉及文件 -- `frontend/src/app/page.tsx` +- `frontend/src/components/home/` - `frontend/src/app/publish/page.tsx` --- -## 🎵 字体与背景音乐资源库接入 (15:50) - -### 资源库 -- `backend/assets/fonts/`(SuperIPAgent 字体全量导入) -- `backend/assets/bgm/`(背景音乐素材) -- `backend/assets/styles/{subtitle.json,title.json}`(样式预设) - -### 服务能力 -- `/api/assets/subtitle-styles`、`/api/assets/title-styles`、`/api/assets/bgm` -- `/assets` 静态挂载供前端预览与试听 - -### 生成链路调整 -- 先完成人声与唇形/字幕对齐,再混入 BGM -- 修复 FFmpeg shell 解析导致的混音失败 -- 禁用 amix 归一化,保证配音音量不被压低 - -### 关键修改 -`backend/app/services/video_service.py` -```python -filter_complex = ( - "[0:a]volume=1.0[a0];" - f"[1:a]volume={volume}[a1];" - "[a0][a1]amix=inputs=2:duration=first:dropout_transition=2:normalize=0[aout]" -) -``` - ---- - -## 🖼️ 标题/字幕样式预览 (16:10) - -### 前端 -- 样式选择 + 预览面板 -- 字号可调(覆盖样式默认值) -- 字体文件动态加载 - -### Remotion -- 样式参数透传到 `Subtitles` / `Title` -- 渲染前临时复制字体到渲染目录 - ---- - ## 📝 文档更新 - [x] `Docs/QWEN3_TTS_DEPLOY.md`: 添加 Flash Attention 安装指南 - [x] `Docs/DEPLOY_MANUAL.md`: 添加 Watchdog 部署说明 - [x] `Docs/task_complete.md`: 更新进度至 100% (Day 16) -- [x] `README.md`: 新增样式与背景音乐能力说明 -- [x] `Docs/BACKEND_README.md`: 资产接口与混音链路说明 -- [x] `Docs/FRONTEND_README.md`: 新增样式预览与BGM试听说明 diff --git a/Docs/DevLogs/Day17.md b/Docs/DevLogs/Day17.md new file mode 100644 index 0000000..4c34166 --- /dev/null +++ b/Docs/DevLogs/Day17.md @@ -0,0 +1,154 @@ +--- + +## 🧩 前端 UI 拆分 (11:00) + +### 内容 +- 首页 `page.tsx` 拆分为独立 UI 组件,状态与逻辑仍集中在页面 +- 新增首页组件目录 `frontend/src/components/home/` + +### 组件列表 +- `HomeHeader` +- `MaterialSelector` +- `ScriptEditor` +- `TitleSubtitlePanel` +- `VoiceSelector` +- `RefAudioPanel` +- `BgmPanel` +- `GenerateActionBar` +- `PreviewPanel` +- `HistoryList` + +--- + +## 🧰 前端通用工具抽取 (11:30) + +### 内容 +- 抽取 API Base / 资源 URL / 日期格式化等通用工具 +- 首页与发布页统一调用,消除重复逻辑 + +### 涉及文件 +- `frontend/src/lib/media.ts` +- `frontend/src/app/page.tsx` +- `frontend/src/app/publish/page.tsx` + +--- + +## 📝 前端规范更新 (11:40) + +### 内容 +- 更新 `FRONTEND_DEV.md` 以匹配最新目录结构 +- 新增 `media.ts` 使用规范与示例 +- 增加组件拆分规范与页面 checklist + +### 涉及文件 +- `Docs/FRONTEND_DEV.md` + +--- + +## 🎨 交互体验与视图优化 (12:00) + +### 主页优化 +- 最新生成作品优先选中并预览 +- 选择项持久化:素材 / 背景音乐 / 历史作品 +- 列表内滚动定位选中项,避免页面跳动 +- 刷新回到顶部(首页) +- 标题/字幕样式预览面板 +- 背景音乐试听即选中,音量滑块实时生效 +- 标题/字幕预览按素材分辨率缩放,字号更接近成片 +- 标题/字幕样式选择持久化,刷新不回默认 +- 默认样式更新:标题 90px 站酷快乐体,字幕 60px 经典黄字 + DingTalkJinBuTi + +### 发布页优化 +- 选择作品改为卡片列表 + 搜索 + 刷新 +- 预览改为弹窗模式 +- 刷新回到顶部(发布页) + +--- + +## 🎵 背景音乐链路修复 (13:00) + +### 修复点 +- FFmpeg 混音改为 `shell=False`,避免 `filter_complex` 被 shell 误解析 +- `amix` 禁用归一化,避免配音音量被压低 + +### 关键修改 +`backend/app/services/video_service.py` + +--- + +## ⚡ 性能微优化 (14:30) + +### 内容 +- 列表渲染启用 `content-visibility`(素材/历史/参考音频/发布作品),BGM 列表保留滚动定位 +- 首屏数据请求并行化(`Promise.allSettled`) +- localStorage 写入防抖(文本/标题/BGM 音量/发布表单) + +--- + +## 🗣️ 字幕断句修复 (13:30) + +### 内容 +- 字幕切分逻辑保留英文单词整体,避免中英混合被硬切 + +### 涉及文件 +- `backend/app/services/whisper_service.py` + +--- + +## 🧱 资源库与样式能力接入 (14:00) + +### 内容 +- 字体库 / BGM 资源接入本地 assets +- 新增样式配置文件(字幕/标题) +- 新增资源 API 与静态挂载 `/assets` +- Remotion 支持样式参数与字体加载 + +### 涉及文件 +- `backend/assets/fonts/` +- `backend/assets/bgm/` +- `backend/assets/styles/subtitle.json` +- `backend/assets/styles/title.json` +- `backend/app/services/assets_service.py` +- `backend/app/api/assets.py` +- `backend/app/main.py` +- `backend/app/api/videos.py` +- `backend/app/services/remotion_service.py` +- `remotion/src/components/Subtitles.tsx` +- `remotion/src/components/Title.tsx` +- `remotion/src/Video.tsx` +- `remotion/render.ts` +- `frontend/src/app/page.tsx` +- `frontend/next.config.ts` + +--- + +## 🖼️ 预览弹窗增强 (15:00) + +### 内容 +- 预览弹窗统一为可复用组件,支持标题与提示 +- 发布页预览与素材预览共享弹窗样式 +- 弹窗头部样式统一(图标 + 标题 + 关闭按钮) + +### 涉及文件 +- `frontend/src/components/VideoPreviewModal.tsx` +- `frontend/src/app/page.tsx` +- `frontend/src/app/publish/page.tsx` + +--- + +## 🧭 术语统一 (15:20) + +### 内容 +- “视频预览” → “作品预览” +- “历史视频” → “历史作品” +- “选择要发布的视频” → “选择要发布的作品” + +--- + +## 🛠️ 运维调整 (15:40) + +### 内容 +- Watchdog 移除 LatentSync 监控,避免长推理误杀 +- LatentSync PM2 增加内存重启阈值(运行时配置) + +--- diff --git a/Docs/Doc_Rules.md b/Docs/Doc_Rules.md index 7f8c6d4..522b92b 100644 --- a/Docs/Doc_Rules.md +++ b/Docs/Doc_Rules.md @@ -24,11 +24,12 @@ | :---: | :--- | :--- | | 🔥 **High** | `Docs/DevLogs/DayN.md` | **(最新日志)** 详细记录变更、修复、代码片段 | | 🔥 **High** | `Docs/task_complete.md` | **(任务总览)** 更新 `[x]`、进度条、时间线 | -| ⚡ **Med** | `README.md` | **(项目主页)** 功能特性、技术栈、最新截图 | -| ⚡ **Med** | `Docs/DEPLOY_MANUAL.md` | **(部署手册)** 环境变量、依赖包、启动命令变更 | -| ⚡ **Med** | `Docs/FRONTEND_DEV.md` | **(前端规范)** API封装、日期格式化、新页面规范 | -| 🧊 **Low** | `Docs/implementation_plan.md` | **(实施计划)** 核对计划与实际实现的差异 | -| 🧊 **Low** | `frontend/README.md` | **(前端文档)** 新页面路由、组件用法、UI变更 | +| ⚡ **Med** | `README.md` | **(项目主页)** 功能特性、技术栈、最新截图 | +| ⚡ **Med** | `Docs/DEPLOY_MANUAL.md` | **(部署手册)** 环境变量、依赖包、启动命令变更 | +| ⚡ **Med** | `Docs/FRONTEND_DEV.md` | **(前端规范)** API封装、日期格式化、新页面规范 | +| ⚡ **Med** | `Docs/FRONTEND_README.md` | **(前端文档)** 功能说明、页面变更 | +| 🧊 **Low** | `Docs/implementation_plan.md` | **(实施计划)** 核对计划与实际实现的差异 | +| 🧊 **Low** | `Docs/architecture_plan.md` | **(前端架构)** 拆分计划与阶段目标 | --- @@ -140,20 +141,20 @@ > **核心原则**:使用正确的工具,避免字符编码问题 -### ✅ 推荐工具:replace_file_content +### ✅ 推荐工具:apply_patch -**使用场景**: +**使用场景**: - 追加新章节到文件末尾 - 修改/替换现有章节内容 - 更新状态标记(🔄 → ✅) - 修正错误内容 -**优势**: +**优势**: - ✅ 自动处理字符编码(Windows CRLF) - ✅ 精确替换,不会误删其他内容 - ✅ 有错误提示,方便调试 -**注意事项**: +**注意事项**: ```markdown 1. **必须精确匹配**:TargetContent 必须与文件完全一致 2. **处理换行符**:文件使用 \r\n,不要漏掉 \r @@ -177,39 +178,45 @@ ### 📝 最佳实践示例 -**追加新章节**: -```python -replace_file_content( - TargetFile="path/to/DayN.md", - TargetContent="## 🔗 相关文档\n\n...\n\n", # 文件末尾的内容 - ReplacementContent="## 🔗 相关文档\n\n...\n\n---\n\n## 🆕 新章节\n内容...", - StartLine=280, - EndLine=284 -) -``` +**追加新章节**: +```diff +*** Begin Patch +*** Update File: Docs/DevLogs/DayN.md +@@ + ## 🔗 相关文档 + + ... +--- + +## 🆕 新章节 +内容... +*** End Patch +``` -**修改现有内容**: -```python -replace_file_content( - TargetContent="**状态**:🔄 待修复", - ReplacementContent="**状态**:✅ 已修复", - StartLine=310, - EndLine=310 -) -``` +**修改现有内容**: +```diff +*** Begin Patch +*** Update File: Docs/DevLogs/DayN.md +@@ +-**状态**:🔄 待修复 ++**状态**:✅ 已修复 +*** End Patch +``` --- -## 📁 文件结构 +## 📁 文件结构 ``` -ViGent/Docs/ -├── task_complete.md # 任务总览(仅按需更新) -├── Doc_Rules.md # 本文件 -├── FRONTEND_DEV.md # 前端开发规范 -├── DEPLOY_MANUAL.md # 部署手册 -├── SUPABASE_DEPLOY.md # Supabase 部署文档 +ViGent2/Docs/ +├── task_complete.md # 任务总览(仅按需更新) +├── Doc_Rules.md # 本文件 +├── FRONTEND_DEV.md # 前端开发规范 +├── FRONTEND_README.md # 前端功能文档 +├── architecture_plan.md # 前端拆分计划 +├── DEPLOY_MANUAL.md # 部署手册 +├── SUPABASE_DEPLOY.md # Supabase 部署文档 └── DevLogs/ ├── Day1.md # 开发日志 └── ... @@ -305,4 +312,4 @@ ViGent/Docs/ --- -**最后更新**:2026-01-23 +**最后更新**:2026-02-04 diff --git a/Docs/FRONTEND_DEV.md b/Docs/FRONTEND_DEV.md index 4adb8f5..cf94553 100644 --- a/Docs/FRONTEND_DEV.md +++ b/Docs/FRONTEND_DEV.md @@ -10,9 +10,13 @@ frontend/src/ │ ├── admin/ # 管理员页面 │ ├── login/ # 登录页面 │ └── register/ # 注册页面 +├── components/ # 可复用组件 +│ ├── home/ # 首页拆分组件 +│ └── ... ├── lib/ # 公共工具函数 │ ├── axios.ts # Axios 实例(含 401/403 拦截器) -│ └── auth.ts # 认证相关函数 +│ ├── auth.ts # 认证相关函数 +│ └── media.ts # API Base / URL / 日期等通用工具 └── proxy.ts # 路由代理(原 middleware) ``` @@ -146,6 +150,26 @@ const { data } = useSWR('/api/xxx', fetcher, { refreshInterval: 2000 }); --- +## 通用工具函数 (media.ts) + +### 统一 API Base / URL 解析 +使用 `@/lib/media` 统一处理服务端/客户端 API Base 与资源地址,避免硬编码: + +```typescript +import { getApiBaseUrl, resolveMediaUrl, resolveAssetUrl, formatDate } from '@/lib/media'; + +const apiBase = getApiBaseUrl(); // SSR: http://localhost:8006 / Client: '' +const playableUrl = resolveMediaUrl(video.path); // 兼容签名 URL 与相对路径 +const fontUrl = resolveAssetUrl(`fonts/${fontFile}`); +const timeText = formatDate(video.created_at); +``` + +### 资源路径规则 +- 视频/音频:优先用 `resolveMediaUrl()` +- 字体/BGM:使用 `resolveAssetUrl()`(自动编码中文路径) + +--- + ## 日期格式化规范 ### 禁止使用 `toLocaleString()` @@ -161,25 +185,46 @@ new Date(timestamp * 1000).toLocaleString('zh-CN') **正确做法:** ```typescript // ✅ 使用固定格式 -const formatDate = (timestamp: number) => { - const d = new Date(timestamp * 1000); - const year = d.getFullYear(); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - const hour = String(d.getHours()).padStart(2, '0'); - const minute = String(d.getMinutes()).padStart(2, '0'); - return `${year}/${month}/${day} ${hour}:${minute}`; -}; +import { formatDate } from '@/lib/media'; ``` --- +## 组件拆分规范 + +当页面组件超过 300-500 行,建议拆分到 `components/`: + +- `page.tsx` 负责状态与业务逻辑 +- 组件只接受 props 与回调,尽量不直接发 API +- 首页拆分组件统一放在 `components/home/` + +--- + +## 用户偏好持久化 + +首页涉及样式与字号等用户偏好时,需持久化并在刷新后恢复: + +- **必须持久化**: + - 标题样式 ID / 字幕样式 ID + - 标题字号 / 字幕字号 + - 背景音乐选择 / 音量 / 开关状态 + - 素材选择 / 历史作品选择 + +### 实施规范 +- 使用 `storageKey = userId || 'guest'`,按用户隔离。 +- **恢复先于保存**:恢复完成前禁止写入(`isRestored` 保护)。 +- 避免默认值覆盖用户选择(优先读取已保存值)。 +- 如需新增持久化字段,必须加入恢复与保存逻辑,并更新本节。 + +--- + ## 新增页面 Checklist 1. [ ] 导入 `import api from '@/lib/axios'` 2. [ ] 所有 API 请求使用 `api.get/post/delete()` 而非原生 `fetch` -3. [ ] 日期格式化使用固定格式函数,不用 `toLocaleString()` -4. [ ] 添加 `'use client'` 指令(如需客户端交互) +3. [ ] 日期格式化使用 `@/lib/media` 的 `formatDate` +4. [ ] 资源 URL 使用 `resolveMediaUrl`/`resolveAssetUrl` +5. [ ] 添加 `'use client'` 指令(如需客户端交互) --- diff --git a/Docs/FRONTEND_README.md b/Docs/FRONTEND_README.md index 823ae24..688bb6a 100644 --- a/Docs/FRONTEND_README.md +++ b/Docs/FRONTEND_README.md @@ -1,6 +1,6 @@ # ViGent2 Frontend -ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。 +ViGent2 的前端界面,采用 Next.js 16 + TailwindCSS 构建。 ## ✨ 核心功能 @@ -11,8 +11,9 @@ ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。 - **标题/字幕样式**: 样式选择 + 预览 + 字号调节 (Day 16)。 - **背景音乐**: 试听 + 音量控制 + 选择持久化 (Day 16)。 - **交互优化**: 选择项持久化、列表内定位、刷新回顶部 (Day 16)。 +- **预览一致性**: 标题/字幕预览按素材分辨率缩放,效果更接近成片 (Day 17)。 - **进度追踪**: 实时显示视频生成进度 (10% -> 100%)。 -- **结果预览**: 生成完成后直接播放下载。 +- **作品预览**: 生成完成后直接播放下载(作品预览 + 历史作品)。 - **本地保存**: 文案/标题自动保存,刷新后恢复 (Day 14)。 ### 2. 全自动发布 (`/publish`) [Day 7 新增] @@ -22,6 +23,7 @@ ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。 - 实时检测扫码状态 (Wait/Success)。 - Cookie 自动保存与状态同步。 - **发布配置**: 设置视频标题、标签、简介。 +- **作品选择**: 卡片列表 + 搜索 + 预览弹窗。 - **定时任务**: 支持 "立即发布" 或 "定时发布"。 ### 3. 声音克隆 [Day 13 新增] @@ -34,6 +36,8 @@ ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。 - **逐字高亮字幕**: 卡拉OK效果,默认开启,可关闭。 - **自动对齐**: 基于 faster-whisper 生成字级别时间戳。 - **样式预设**: 标题/字幕样式选择 + 预览 + 字号调节 (Day 16)。 +- **默认样式**: 标题 90px 站酷快乐体;字幕 60px 经典黄字 + DingTalkJinBuTi (Day 17)。 +- **样式持久化**: 标题/字幕样式与字号刷新保留 (Day 17)。 ### 5. 背景音乐 [Day 16 新增] - **试听预览**: 点击试听即选中,音量滑块实时生效。 @@ -52,7 +56,7 @@ ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。 ## 🛠️ 技术栈 -- **框架**: Next.js 14 (App Router) +- **框架**: Next.js 16 (App Router) - **样式**: TailwindCSS - **图标**: Lucide React - **组件**: 自定义现代化组件 (Glassmorphism 风格) @@ -85,15 +89,16 @@ src/ │ │ └── page.tsx │ └── layout.tsx # 全局布局 (导航栏) ├── components/ # UI 组件 -│ ├── VideoUploader.tsx # 视频上传 -│ ├── StatusBadge.tsx # 状态徽章 +│ ├── home/ # 首页拆分组件 │ └── ... └── lib/ # 工具函数 + └── media.ts # API Base / URL / 日期等通用工具 ``` ## 🔌 后端对接 -- **Base URL**: `http://localhost:8006` +- **Base URL**: `http://localhost:8006` (SSR) / 相对路径 (Client) +- **URL 统一工具**: `@/lib/media` 提供 `resolveMediaUrl` / `resolveAssetUrl` - **代理配置**: Next.js Rewrites (如需) 或直接 CORS。 ## 🎨 设计规范 diff --git a/Docs/implementation_plan.md b/Docs/implementation_plan.md index 83b4c0b..e921803 100644 --- a/Docs/implementation_plan.md +++ b/Docs/implementation_plan.md @@ -42,17 +42,28 @@ | 模块 | 技术选择 | 备选方案 | |------|----------|----------| -| **前端框架** | Next.js 14 | Vue 3 + Vite | -| **UI 组件库** | Tailwind + shadcn/ui | Ant Design | -| **后端框架** | FastAPI | Flask | -| **任务队列** | Celery + Redis | RQ / Dramatiq | -| **唇形同步** | **LatentSync 1.6** | MuseTalk / Wav2Lip | -| **TTS 配音** | EdgeTTS | CosyVoice | -| **声音克隆** | **Qwen3-TTS 1.7B** ✅ | GPT-SoVITS | -| **视频处理** | FFmpeg | MoviePy | -| **自动发布** | social-auto-upload | 自行实现 | -| **数据库** | SQLite → PostgreSQL | MySQL | -| **文件存储** | 本地 / MinIO | 阿里云 OSS | +| **前端框架** | Next.js 16 | Vue 3 + Vite | +| **UI 组件库** | TailwindCSS (自定义组件) | Ant Design | +| **后端框架** | FastAPI | Flask | +| **任务队列** | FastAPI BackgroundTasks (asyncio) | Celery + Redis | +| **唇形同步** | **LatentSync 1.6** | MuseTalk / Wav2Lip | +| **TTS 配音** | EdgeTTS | CosyVoice | +| **声音克隆** | **Qwen3-TTS 1.7B** ✅ | GPT-SoVITS | +| **视频处理** | FFmpeg | MoviePy | +| **自动发布** | Playwright | 自行实现 | +| **数据库** | Supabase (PostgreSQL) | MySQL | +| **文件存储** | Supabase Storage | 阿里云 OSS | + +> **修正 (18:10)**:当前实现采用 Next.js 16、FastAPI BackgroundTasks 与 Supabase Storage/Auth,自动发布基于 Playwright。 + +--- + +## ✅ 现状补充 (Day 17) + +- 前端已拆分为组件化结构(`components/home/`),主页面逻辑集中。 +- 通用工具 `media.ts` 统一处理 API Base / 资源 URL / 日期格式化。 +- 作品预览弹窗统一样式,并支持素材/发布预览复用。 +- 标题/字幕预览按素材分辨率缩放,效果更接近成片。 --- @@ -60,24 +71,11 @@ ### 阶段一:核心功能验证 (MVP) -> **目标**:验证 MuseTalk + EdgeTTS 效果,跑通端到端流程 +> **目标**:验证 LatentSync + EdgeTTS 效果,跑通端到端流程 -#### 1.1 环境搭建 - -```bash -# 创建项目目录 -mkdir TalkingHeadAgent -cd TalkingHeadAgent - -# 克隆 MuseTalk -git clone https://github.com/TMElyralab/MuseTalk.git - -# 安装依赖 -cd MuseTalk -pip install -r requirements.txt - -# 下载模型权重 (按官方文档) -``` +#### 1.1 环境搭建 + +参考 `models/LatentSync/DEPLOY.md` 完成 LatentSync 环境与权重部署。 #### 1.2 集成 EdgeTTS @@ -98,13 +96,13 @@ async def text_to_speech(text: str, voice: str = "zh-CN-YunxiNeural", output_pat # test_pipeline.py """ 1. 文案 → EdgeTTS → 音频 -2. 静态视频 + 音频 → MuseTalk → 口播视频 +2. 静态视频 + 音频 → LatentSync → 口播视频 3. 添加字幕 → FFmpeg → 最终视频 """ ``` #### 1.4 验证标准 -- [ ] MuseTalk 能正常推理 +- [ ] LatentSync 能正常推理 - [ ] 唇形与音频同步率 > 90% - [ ] 单个视频生成时间 < 2 分钟 @@ -145,22 +143,16 @@ backend/ | `/api/materials` | POST | 上传素材视频 | ✅ | | `/api/materials` | GET | 获取素材列表 | ✅ | | `/api/videos/generate` | POST | 创建视频生成任务 | ✅ | -| `/api/tasks/{id}` | GET | 查询任务状态 | ✅ | -| `/api/videos/{id}/download` | GET | 下载生成的视频 | ✅ | +| `/api/videos/tasks/{id}` | GET | 查询任务状态 | ✅ | +| `/api/videos/generated` | GET | 获取历史作品列表 | ✅ | | `/api/publish` | POST | 发布到社交平台 | ✅ | -#### 2.3 Celery 任务定义 - -```python -# tasks/celery_tasks.py -@celery.task -def generate_video_task(material_id: str, text: str, voice: str): - # 1. TTS 生成音频 - # 2. MuseTalk 唇形同步 - # 3. FFmpeg 添加字幕 - # 4. 保存并返回视频 URL - pass -``` +#### 2.3 BackgroundTasks 任务定义 + +```python +# app/api/videos.py +background_tasks.add_task(_process_video_generation, task_id, req, user_id) +``` --- @@ -183,9 +175,9 @@ def generate_video_task(material_id: str, text: str, voice: str): # 创建 Next.js 项目 npx create-next-app@latest frontend --typescript --tailwind --app -# 安装依赖 -cd frontend -npm install @tanstack/react-query axios +# 安装依赖 +cd frontend +npm install axios swr ``` --- diff --git a/Docs/task_complete.md b/Docs/task_complete.md index a503138..3c1df61 100644 --- a/Docs/task_complete.md +++ b/Docs/task_complete.md @@ -1,8 +1,8 @@ # ViGent2 开发任务清单 (Task Log) **项目**: ViGent2 数字人口播视频生成系统 -**进度**: 100% (Day 16 - 深度优化完成) -**更新时间**: 2026-02-03 +**进度**: 100% (Day 17 - 前端重构与体验优化) +**更新时间**: 2026-02-04 --- @@ -10,15 +10,23 @@ > 这里记录了每一天的核心开发内容与 milestone。 -### Day 16: 深度性能优化 (Current) 🚀 +### Day 17: 前端重构与体验优化 (Current) 🚀 +- [x] **UI 组件拆分**: 首页拆分为独立组件,降低 `page.tsx` 复杂度。 +- [x] **通用工具抽取**: `media.ts` 统一 API Base / URL / 日期格式化。 +- [x] **交互优化**: 选择项持久化、列表内定位、刷新回顶部、最新作品优先预览。 +- [x] **发布页改造**: 作品列表卡片化 + 搜索 + 预览弹窗。 +- [x] **预览体验**: 预览弹窗统一头部样式与提示文案。 +- [x] **预览一致性**: 标题/字幕预览按素材分辨率缩放。 +- [x] **样式默认与持久化**: 默认样式与字号调整,刷新保留用户选择。 +- [x] **性能微优化**: 列表渲染优化 + 并行请求 + localStorage 防抖。 +- [x] **资源能力**: 字体/BGM 资源库 + `/api/assets` 接入。 +- [x] **音频与字幕修复**: BGM 混音稳定性与字幕断句优化。 + +### Day 16: 深度性能优化 - [x] **Qwen-TTS 加速**: 集成 Flash Attention 2,模型加载速度提升至 8.9s。 - [x] **服务守护**: 开发 `Watchdog` 看门狗机制,自动监控并重启僵死服务。 - [x] **LatentSync 性能确认**: 验证 DeepCache + 原生 Flash Attn 生效。 - [x] **文档重构**: 全面更新 README、部署手册及后端文档。 -- [x] **UI 交互优化**: 选择项持久化、列表内定位、刷新回顶部。 -- [x] **样式与预览**: 标题/字幕样式选择 + 预览 + 字号调节。 -- [x] **背景音乐**: 试听 + 音量控制 + 混音稳定性修复。 -- [x] **资产库接入**: 字体/BGM 资源库 + `/api/assets` 资源接口。 ### Day 15: 手机号认证迁移 - [x] **认证系统升级**: 从邮箱迁移至 11 位手机号注册/登录。 diff --git a/README.md b/README.md index e66db4e..84cef4e 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - 🎙️ **多模态配音** - 支持 **EdgeTTS** (微软超自然语音) 和 **Qwen3-TTS** (3秒极速声音克隆)。 - 📝 **智能字幕** - 集成 faster-whisper + Remotion,自动生成逐字高亮 (卡拉OK效果) 字幕。 - 🎨 **样式预设** - 标题/字幕样式选择 + 预览 + 字号调节,支持自定义字体库。 +- 🖼️ **作品预览一致性** - 标题/字幕预览按素材分辨率缩放,效果更接近成片。 - 🎵 **背景音乐** - 试听 + 音量控制 + 混音,保持配音音量稳定。 - 🤖 **AI 辅助创作** - 内置 GLM-4.7-Flash,支持 B站/抖音链接文案提取、AI 洗稿、标题/标签自动生成。 @@ -35,7 +36,7 @@ | 领域 | 核心技术 | 说明 | |------|----------|------| -| **前端** | Next.js 14 | TypeScript, TailwindCSS, SWR | +| **前端** | Next.js 16 | TypeScript, TailwindCSS, SWR | | **后端** | FastAPI | Python 3.10, AsyncIO, PM2 | | **数据库** | Supabase | PostgreSQL, Storage (本地/S3), Auth | | **唇形同步** | LatentSync 1.6 | PyTorch 2.5, Diffusers, DeepCache | diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 28b9042..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# ViGent2 Frontend - -ViGent2 的前端界面,采用 Next.js 14 + TailwindCSS 构建。 - -## ✨ 核心功能 - -### 1. 视频生成 (`/`) -- **素材管理**: 拖拽上传人物视频,实时预览。 -- **文案配音**: 集成 EdgeTTS,支持多音色选择 (云溪 / 晓晓)。 -- **AI 标题/标签**: 一键生成视频标题与标签 (Day 14)。 -- **进度追踪**: 实时显示视频生成进度 (10% -> 100%)。 -- **结果预览**: 生成完成后直接播放下载。 -- **本地保存**: 文案/标题自动保存,刷新后恢复 (Day 14)。 - -### 2. 全自动发布 (`/publish`) [Day 7 新增] -- **多平台管理**: 统一管理 B站、抖音、小红书账号状态。 -- **扫码登录**: - - 集成后端 Playwright 生成的 QR Code。 - - 实时检测扫码状态 (Wait/Success)。 - - Cookie 自动保存与状态同步。 -- **发布配置**: 设置视频标题、标签、简介。 -- **定时任务**: 支持 "立即发布" 或 "定时发布"。 - -### 3. 声音克隆 [Day 13 新增] -- **TTS 模式选择**: EdgeTTS (预设音色) / 声音克隆 (自定义音色) 切换。 -- **参考音频管理**: 上传/列表/删除参考音频 (3-20秒 WAV)。 -- **一键克隆**: 选择参考音频后自动调用 Qwen3-TTS 服务。 - -### 4. 字幕与标题 [Day 13 新增] -- **片头标题**: 可选输入,视频开头显示 3 秒淡入淡出标题。 -- **逐字高亮字幕**: 卡拉OK效果,默认开启,可关闭。 -- **自动对齐**: 基于 faster-whisper 生成字级别时间戳。 - -### 5. 账户设置 [Day 15 新增] -- **手机号登录**: 11位中国手机号验证登录。 -- **账户下拉菜单**: 显示有效期 + 修改密码 + 安全退出。 -- **修改密码**: 弹窗输入当前密码与新密码,修改后强制重新登录。 - -### 6. 文案提取助手 (`ScriptExtractionModal`) [Day 15 新增] -- **多源提取**: 支持文件拖拽上传与 URL 粘贴 (B站/抖音/TikTok)。 -- **AI 洗稿**: 集成 GLM-4.7-Flash,自动改写为口播文案。 -- **一键填入**: 提取结果直接填充至视频生成输入框。 -- **智能交互**: 实时进度展示,防误触设计。 - -## 🛠️ 技术栈 - -- **框架**: Next.js 14 (App Router) -- **样式**: TailwindCSS -- **图标**: Lucide React -- **组件**: 自定义现代化组件 (Glassmorphism 风格) -- **API**: Axios 实例 `@/lib/axios` (对接后端 FastAPI :8006) - -## 🚀 开发指南 - -### 安装依赖 - -```bash -npm install -``` - -### 启动开发服务器 - -默认运行在 **3002** 端口 (通过 `package.json` 配置): - -```bash -npm run dev -# 访问: http://localhost:3002 -``` - -### 目录结构 - -``` -src/ -├── app/ -│ ├── page.tsx # 视频生成主页 -│ ├── publish/ # 发布管理页 -│ │ └── page.tsx -│ └── layout.tsx # 全局布局 (导航栏) -├── components/ # UI 组件 -│ ├── VideoUploader.tsx # 视频上传 -│ ├── StatusBadge.tsx # 状态徽章 -│ └── ... -└── lib/ # 工具函数 -``` - -## 🔌 后端对接 - -- **Base URL**: `http://localhost:8006` -- **代理配置**: Next.js Rewrites (如需) 或直接 CORS。 - -## 🎨 设计规范 - -- **主色调**: 深紫/黑色系 (Dark Mode) -- **交互**: 悬停微动画 (Hover Effects) -- **响应式**: 适配桌面端大屏操作 diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 2480ff2..1f1bc8e 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2,82 +2,42 @@ "use client"; import { useState, useEffect, useRef } from "react"; -import Link from "next/link"; import api from "@/lib/axios"; +import { + getApiBaseUrl, + resolveMediaUrl, + resolveAssetUrl, + resolveBgmUrl, + getFontFormat, + buildTextShadow, + formatDate, +} from "@/lib/media"; import { useAuth } from "@/contexts/AuthContext"; import { useTask } from "@/contexts/TaskContext"; -import AccountSettingsDropdown from "@/components/AccountSettingsDropdown"; import VideoPreviewModal from "@/components/VideoPreviewModal"; import ScriptExtractionModal from "@/components/ScriptExtractionModal"; -import { - Upload, - RefreshCw, - Eye, - Trash2, - FileText, - Sparkles, - Loader2, - Volume2, - Mic, - Play, - Pause, - Pencil, - Check, - X, - Square, - Rocket, - Download, - Send, -} from "lucide-react"; +import { HomeHeader } from "@/components/home/HomeHeader"; +import { MaterialSelector } from "@/components/home/MaterialSelector"; +import { ScriptEditor } from "@/components/home/ScriptEditor"; +import { TitleSubtitlePanel } from "@/components/home/TitleSubtitlePanel"; +import { VoiceSelector } from "@/components/home/VoiceSelector"; +import { RefAudioPanel } from "@/components/home/RefAudioPanel"; +import { BgmPanel } from "@/components/home/BgmPanel"; +import { GenerateActionBar } from "@/components/home/GenerateActionBar"; +import { PreviewPanel } from "@/components/home/PreviewPanel"; +import { HistoryList } from "@/components/home/HistoryList"; -const API_BASE = typeof window === 'undefined' - ? (process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8006') - : ''; +const API_BASE = getApiBaseUrl(); -const isAbsoluteUrl = (url: string) => /^https?:\/\//i.test(url); +const VOICES = [ + { id: "zh-CN-YunxiNeural", name: "云溪 (男声-年轻)" }, + { id: "zh-CN-YunjianNeural", name: "云健 (男声-新闻)" }, + { id: "zh-CN-YunyangNeural", name: "云扬 (男声-专业)" }, + { id: "zh-CN-XiaoxiaoNeural", name: "晓晓 (女声-活泼)" }, + { id: "zh-CN-XiaoyiNeural", name: "晓伊 (女声-温柔)" }, +]; -const joinBaseUrl = (base: string, path: string) => { - if (!base) return path; - if (!path.startsWith('/')) return `${base}/${path}`; - return `${base}${path}`; -}; - -const resolveMediaUrl = (url?: string | null) => { - if (!url) return null; - if (isAbsoluteUrl(url)) return url; - return joinBaseUrl(API_BASE, url); -}; - -const encodePathSegments = (value: string) => value.split('/').map(encodeURIComponent).join('/'); - -const resolveAssetUrl = (assetPath?: string | null) => { - if (!assetPath) return null; - const encoded = encodePathSegments(assetPath); - return joinBaseUrl(API_BASE, `/assets/${encoded}`); -}; - -const resolveBgmUrl = (bgmId?: string | null) => { - if (!bgmId) return null; - return resolveAssetUrl(`bgm/${bgmId}`); -}; - -const getFontFormat = (fontFile?: string) => { - if (!fontFile) return 'truetype'; - const ext = fontFile.split('.').pop()?.toLowerCase(); - if (ext === 'otf') return 'opentype'; - return 'truetype'; -}; - -const buildTextShadow = (color: string, size: number) => { - return [ - `-${size}px -${size}px 0 ${color}`, - `${size}px -${size}px 0 ${color}`, - `-${size}px ${size}px 0 ${color}`, - `${size}px ${size}px 0 ${color}`, - `0 0 ${size * 4}px rgba(0,0,0,0.9)`, - `0 4px 8px rgba(0,0,0,0.6)` - ].join(','); -}; +const FIXED_REF_TEXT = "其实生活中有许多美好的瞬间,比如清晨的阳光,或者一杯温热的清茶。希望这次生成的音色能够自然、流畅,完美还原出我最真实的声音状态。"; const scrollContainerToItem = (container: HTMLDivElement, item: HTMLDivElement) => { const containerRect = container.getBoundingClientRect(); @@ -164,16 +124,6 @@ interface BgmItem { ext?: string; } -// 格式化日期(避免 Hydration 错误) -const formatDate = (timestamp: number) => { - const d = new Date(timestamp * 1000); - const year = d.getFullYear(); - const month = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); - const hour = String(d.getHours()).padStart(2, '0'); - const minute = String(d.getMinutes()).padStart(2, '0'); - return `${year}/${month}/${day} ${hour}:${minute}`; -}; @@ -206,11 +156,13 @@ export default function Home() { const [titleStyles, setTitleStyles] = useState([]); const [selectedSubtitleStyleId, setSelectedSubtitleStyleId] = useState(""); const [selectedTitleStyleId, setSelectedTitleStyleId] = useState(""); - const [subtitleFontSize, setSubtitleFontSize] = useState(52); - const [titleFontSize, setTitleFontSize] = useState(72); + const [subtitleFontSize, setSubtitleFontSize] = useState(60); + const [titleFontSize, setTitleFontSize] = useState(90); const [subtitleSizeLocked, setSubtitleSizeLocked] = useState(false); const [titleSizeLocked, setTitleSizeLocked] = useState(false); const [showStylePreview, setShowStylePreview] = useState(false); + const [materialDimensions, setMaterialDimensions] = useState<{ width: number; height: number } | null>(null); + const [previewContainerWidth, setPreviewContainerWidth] = useState(0); // 背景音乐相关状态 const [bgmList, setBgmList] = useState([]); @@ -237,6 +189,7 @@ export default function Home() { const bgmPlayerRef = useRef(null); const bgmItemRefs = useRef>({}); const bgmListContainerRef = useRef(null); + const titlePreviewContainerRef = useRef(null); const materialItemRefs = useRef>({}); const videoItemRefs = useRef>({}); @@ -360,17 +313,6 @@ export default function Home() { const [extractModalOpen, setExtractModalOpen] = useState(false); - // 可选音色 - const voices = [ - { id: "zh-CN-YunxiNeural", name: "云溪 (男声-年轻)" }, - { id: "zh-CN-YunjianNeural", name: "云健 (男声-新闻)" }, - { id: "zh-CN-YunyangNeural", name: "云扬 (男声-专业)" }, - { id: "zh-CN-XiaoxiaoNeural", name: "晓晓 (女声-活泼)" }, - { id: "zh-CN-XiaoyiNeural", name: "晓伊 (女声-温柔)" }, - ]; - - // 声音克隆固定参考文字(用户录音/上传时需要读这段话) - const FIXED_REF_TEXT = "其实生活中有许多美好的瞬间,比如清晨的阳光,或者一杯温热的清茶。希望这次生成的音色能够自然、流畅,完美还原出我最真实的声音状态。"; // 获取存储 key 的前缀(登录用户使用 userId,未登录使用 guest) const storageKey = userId || 'guest'; @@ -378,14 +320,75 @@ export default function Home() { // 加载素材列表和历史视频 useEffect(() => { if (isAuthLoading) return; - fetchMaterials(); - fetchGeneratedVideos(); - fetchRefAudios(); - fetchSubtitleStyles(); - fetchTitleStyles(); - fetchBgmList(); + void Promise.allSettled([ + fetchMaterials(), + fetchGeneratedVideos(), + fetchRefAudios(), + fetchSubtitleStyles(), + fetchTitleStyles(), + fetchBgmList(), + ]); }, [isAuthLoading]); + useEffect(() => { + const material = materials.find((item) => item.id === selectedMaterial); + if (!material?.path) { + setMaterialDimensions(null); + return; + } + const url = resolveMediaUrl(material.path); + if (!url) { + setMaterialDimensions(null); + return; + } + + let isActive = true; + const video = document.createElement('video'); + video.crossOrigin = 'anonymous'; + video.preload = 'metadata'; + video.src = url; + video.load(); + + const handleLoaded = () => { + if (!isActive) return; + if (video.videoWidth && video.videoHeight) { + setMaterialDimensions({ width: video.videoWidth, height: video.videoHeight }); + } else { + setMaterialDimensions(null); + } + }; + + const handleError = () => { + if (!isActive) return; + setMaterialDimensions(null); + }; + + video.addEventListener('loadedmetadata', handleLoaded); + video.addEventListener('error', handleError); + + return () => { + isActive = false; + video.removeEventListener('loadedmetadata', handleLoaded); + video.removeEventListener('error', handleError); + }; + }, [selectedMaterial, materials]); + + useEffect(() => { + if (!showStylePreview) return; + const container = titlePreviewContainerRef.current; + if (!container) return; + + const observer = new ResizeObserver((entries) => { + const entry = entries[0]; + if (entry) { + setPreviewContainerWidth(entry.contentRect.width); + } + }); + + observer.observe(container); + return () => observer.disconnect(); + }, [showStylePreview]); + useEffect(() => { if (typeof window === 'undefined') return; if ('scrollRestoration' in window.history) { @@ -469,17 +472,19 @@ export default function Home() { // 保存用户输入到 localStorage(恢复完成后才保存,未登录用户也可保存) useEffect(() => { - if (isRestored) { - console.log("[Home] 保存 text:", text.substring(0, 50) + "..."); + if (!isRestored) return; + const timeout = setTimeout(() => { localStorage.setItem(`vigent_${storageKey}_text`, text); - } + }, 300); + return () => clearTimeout(timeout); }, [text, storageKey, isRestored]); useEffect(() => { - if (isRestored) { - console.log("[Home] 保存 title:", videoTitle); + if (!isRestored) return; + const timeout = setTimeout(() => { localStorage.setItem(`vigent_${storageKey}_title`, videoTitle); - } + }, 300); + return () => clearTimeout(timeout); }, [videoTitle, storageKey, isRestored]); useEffect(() => { @@ -531,9 +536,11 @@ export default function Home() { }, [selectedBgmId, storageKey, isRestored]); useEffect(() => { - if (isRestored) { + if (!isRestored) return; + const timeout = setTimeout(() => { localStorage.setItem(`vigent_${storageKey}_bgmVolume`, String(bgmVolume)); - } + }, 300); + return () => clearTimeout(timeout); }, [bgmVolume, storageKey, isRestored]); useEffect(() => { @@ -625,10 +632,13 @@ export default function Home() { const { data } = await api.get('/api/assets/subtitle-styles'); const styles: SubtitleStyleOption[] = data.styles || []; setSubtitleStyles(styles); - if (!selectedSubtitleStyleId) { - const defaultStyle = styles.find(s => s.is_default) || styles[0]; - if (defaultStyle) setSelectedSubtitleStyleId(defaultStyle.id); - } + const savedStyleId = localStorage.getItem(`vigent_${storageKey}_subtitleStyle`); + setSelectedSubtitleStyleId((prev) => { + if (prev && styles.some((s) => s.id === prev)) return prev; + if (savedStyleId && styles.some((s) => s.id === savedStyleId)) return savedStyleId; + const defaultStyle = styles.find((s) => s.is_default) || styles[0]; + return defaultStyle?.id || ""; + }); } catch (error) { console.error("获取字幕样式失败:", error); } @@ -640,10 +650,13 @@ export default function Home() { const { data } = await api.get('/api/assets/title-styles'); const styles: TitleStyleOption[] = data.styles || []; setTitleStyles(styles); - if (!selectedTitleStyleId) { - const defaultStyle = styles.find(s => s.is_default) || styles[0]; - if (defaultStyle) setSelectedTitleStyleId(defaultStyle.id); - } + const savedStyleId = localStorage.getItem(`vigent_${storageKey}_titleStyle`); + setSelectedTitleStyleId((prev) => { + if (prev && styles.some((s) => s.id === prev)) return prev; + if (savedStyleId && styles.some((s) => s.id === savedStyleId)) return savedStyleId; + const defaultStyle = styles.find((s) => s.is_default) || styles[0]; + return defaultStyle?.id || ""; + }); } catch (error) { console.error("获取标题样式失败:", error); } @@ -1064,988 +1077,182 @@ export default function Home() { } }; - const activeSubtitleStyle = subtitleStyles.find(s => s.id === selectedSubtitleStyleId) - || subtitleStyles.find(s => s.is_default) - || subtitleStyles[0]; - - const activeTitleStyle = titleStyles.find(s => s.id === selectedTitleStyleId) - || titleStyles.find(s => s.is_default) - || titleStyles[0]; - - const previewTitleText = videoTitle.trim() || "这里是标题预览"; - const subtitleHighlightText = "最近,一个叫Cloudbot"; - const subtitleNormalText = "的开源项目在GitHub上彻底火了"; - - const subtitleHighlightColor = activeSubtitleStyle?.highlight_color || "#FFE600"; - const subtitleNormalColor = activeSubtitleStyle?.normal_color || "#FFFFFF"; - const subtitleStrokeColor = activeSubtitleStyle?.stroke_color || "#000000"; - const subtitleStrokeSize = activeSubtitleStyle?.stroke_size ?? 3; - const subtitleLetterSpacing = activeSubtitleStyle?.letter_spacing ?? 2; - const subtitleFontFamilyName = `SubtitlePreview-${activeSubtitleStyle?.id || "default"}`; - const subtitleFontUrl = activeSubtitleStyle?.font_file - ? resolveAssetUrl(`fonts/${activeSubtitleStyle.font_file}`) - : null; - - const titleColor = activeTitleStyle?.color || "#FFFFFF"; - const titleStrokeColor = activeTitleStyle?.stroke_color || "#000000"; - const titleStrokeSize = activeTitleStyle?.stroke_size ?? 8; - const titleLetterSpacing = activeTitleStyle?.letter_spacing ?? 4; - const titleFontWeight = activeTitleStyle?.font_weight ?? 900; - const titleFontFamilyName = `TitlePreview-${activeTitleStyle?.id || "default"}`; - const titleFontUrl = activeTitleStyle?.font_file - ? resolveAssetUrl(`fonts/${activeTitleStyle.font_file}`) - : null; - return (
- {/* Header
-
-

- 🎬 - IPAgent -

-
- - 视频生成 - - - 发布管理 - -
-
-
*/} -
-
- - 🎬 - IPAgent - -
- - 视频生成 - - - 发布管理 - - {/* 账户设置下拉菜单 */} - -
-
-
+
{/* 左侧: 输入区域 */}
{/* 素材选择 */} -
-
-

- 📹 选择素材视频 - - (上传自拍视频) - -

-
- {/* 隐藏的文件输入 */} - - - -
-
- - {/* 上传进度条 */} - {isUploading && ( -
-
- 📤 上传中... - {uploadProgress}% -
-
-
-
-
- )} - - {/* 上传错误提示 */} - {uploadError && ( -
- ❌ {uploadError} - -
- )} - - {fetchError ? ( -
- 获取素材失败: {fetchError} -
- API: {API_BASE}/api/materials/ -
- ) : materials.length === 0 ? ( -
-
📁
-

暂无素材视频

-

- 点击上方「📤 上传视频」按钮添加素材 -

-
- ) : ( -
- {materials.map((m) => ( -
{ - materialItemRefs.current[m.id] = el; - }} - className={`p-3 rounded-lg border transition-all flex items-center justify-between group ${selectedMaterial === m.id - ? "border-purple-500 bg-purple-500/20" - : "border-white/10 bg-white/5 hover:border-white/30" - }`} - > - -
- - -
-
- ))} -
- )} -
+ { + setPreviewMaterial(resolveMediaUrl(path)); + }} + onDeleteMaterial={deleteMaterial} + onClearUploadError={() => setUploadError(null)} + registerMaterialRef={(id, el) => { + materialItemRefs.current[id] = el; + }} + /> {/* 文案输入 */} -
-
-

- ✍️ 文案提取与编辑 -

-
- - -
-
-