diff --git a/Docs/DevLogs/Day27.md b/Docs/DevLogs/Day27.md new file mode 100644 index 0000000..c965c66 --- /dev/null +++ b/Docs/DevLogs/Day27.md @@ -0,0 +1,86 @@ +## Remotion 描边修复 + 字体样式扩展 + TypeScript 修复 (Day 27) + +### 概述 + +修复标题/字幕描边渲染问题(描边过粗 + 副标题重影),扩展字体样式选项(标题 4→12、字幕 4→8),修复 Remotion 项目 TypeScript 类型错误。 + +--- + +## ✅ 改动内容 + +### 1. 描边渲染修复(标题 + 字幕) + +- **问题**: 标题黑色描边过粗,副标题出现重影/鬼影 +- **根因**: `buildTextShadow` 用 4 方向 `textShadow` 模拟描边 — 对角线叠加导致描边视觉上比实际 `stroke_size` 更粗;4 角方向在中间有间隙和叠加,造成重影 +- **修复**: 改用 CSS 原生描边 `-webkit-text-stroke` + `paint-order: stroke fill`(Remotion 用 Chromium 渲染,完美支持) +- **旧方案**: + ```javascript + textShadow: `-8px -8px 0 #000, 8px -8px 0 #000, -8px 8px 0 #000, 8px 8px 0 #000, 0 0 16px rgba(0,0,0,0.5), 0 2px 4px rgba(0,0,0,0.3)` + ``` +- **新方案**: + ```javascript + WebkitTextStroke: `5px #000000`, + paintOrder: 'stroke fill', + textShadow: `0 2px 4px rgba(0,0,0,0.3)`, + ``` +- 同时将所有预设样式的 `stroke_size` 从 8 降到 5,配合原生描边视觉更干净 + +### 2. 字体样式扩展 + +**标题样式**: 4 个 → 12 个(+8) + +| ID | 样式名 | 字体 | 配色 | +|----|--------|------|------| +| title_pangmen | 庞门正道 | 庞门正道标题体3.0 | 白字黑描 | +| title_round | 优设标题圆 | 优设标题圆 | 白字紫描 | +| title_alibaba | 阿里数黑体 | 阿里巴巴数黑体 | 白字黑描 | +| title_chaohei | 文道潮黑 | 文道潮黑 | 青蓝字深蓝描 | +| title_wujie | 无界黑 | 标小智无界黑 | 白字深灰描 | +| title_houdi | 厚底黑 | Aa厚底黑 | 红字深黑描 | +| title_banyuan | 寒蝉半圆体 | 寒蝉半圆体 | 白字黑描 | +| title_jixiang | 欣意吉祥宋 | 字体圈欣意吉祥宋 | 金字棕描 | + +**字幕样式**: 4 个 → 8 个(+4) + +| ID | 样式名 | 字体 | 高亮色 | +|----|--------|------|--------| +| subtitle_pink | 少女粉 | DingTalk JinBuTi | 粉色 #FF69B4 | +| subtitle_lime | 清新绿 | DingTalk Sans | 荧光绿 #76FF03 | +| subtitle_gold | 金色隶书 | 阿里妈妈刀隶体 | 金色 #FDE68A | +| subtitle_kai | 楷体红字 | SimKai | 红色 #FF4444 | + +### 3. TypeScript 类型错误修复 + +- **Root.tsx**: `Composition` 泛型类型与 `calculateMetadata` 参数类型不匹配 — 内联 `calculateMetadata` 并显式标注参数类型,`defaultProps` 使用 `satisfies VideoProps` 约束 +- **Video.tsx**: `VideoProps` 接口添加 `[key: string]: unknown` 索引签名,兼容 Remotion 要求的 `Record` 约束 +- **VideoLayer.tsx**: `OffthreadVideo` 组件不支持 `loop` prop — 移除(该 prop 原本就被忽略) + +### 4. 进度条文案还原 + +- **问题**: 进度条显示后端推送的详细阶段消息(如"正在合成唇型"),用户希望只显示"正在AI生成中..." +- **修复**: `HomePage.tsx` 进度条文案从 `{currentTask.message || "正在AI生成中..."}` 改为固定 `正在AI生成中...` + +--- + +## 📁 修改文件清单 + +| 文件 | 改动 | +|------|------| +| `remotion/src/components/Title.tsx` | `buildTextShadow` → `buildStrokeStyle`(CSS 原生描边),标题+副标题同时生效 | +| `remotion/src/components/Subtitles.tsx` | `buildTextShadow` → `buildStrokeStyle`(CSS 原生描边) | +| `remotion/src/Root.tsx` | 修复 `Composition` 泛型类型、`calculateMetadata` 参数类型 | +| `remotion/src/Video.tsx` | `VideoProps` 添加索引签名 | +| `remotion/src/components/VideoLayer.tsx` | 移除 `OffthreadVideo` 不支持的 `loop` prop | +| `backend/assets/styles/title.json` | 标题样式从 4 个扩展到 12 个,`stroke_size` 8→5 | +| `backend/assets/styles/subtitle.json` | 字幕样式从 4 个扩展到 8 个 | +| `frontend/.../HomePage.tsx` | 进度条文案还原为固定"正在AI生成中..." | + +--- + +## 🔍 验证 + +- `npx tsc --noEmit` — 零错误 +- `npm run build:render` — 渲染脚本编译成功 +- `npm run build`(前端)— 零报错 +- 描边:标题/副标题/字幕使用 CSS 原生描边,无重影、无虚胖 +- 样式选择:前端下拉可加载全部 12 个标题 + 8 个字幕样式 diff --git a/Docs/task_complete.md b/Docs/task_complete.md index 4bb64e3..d592408 100644 --- a/Docs/task_complete.md +++ b/Docs/task_complete.md @@ -1,8 +1,8 @@ # ViGent2 开发任务清单 (Task Log) **项目**: ViGent2 数字人口播视频生成系统 -**进度**: 100% (Day 26 - 前端优化:板块合并 + 序号标题) -**更新时间**: 2026-02-25 +**进度**: 100% (Day 27 - Remotion 描边修复 + 字体样式扩展) +**更新时间**: 2026-02-26 --- @@ -10,7 +10,14 @@ > 这里记录了每一天的核心开发内容与 milestone。 -### Day 26: 前端优化:板块合并 + 序号标题 + UI 精细化 (Current) +### Day 27: Remotion 描边修复 + 字体样式扩展 (Current) +- [x] **描边渲染修复**: 标题/副标题/字幕从 `textShadow` 4 方向模拟改为 CSS 原生 `-webkit-text-stroke` + `paint-order: stroke fill`,修复描边过粗和副标题重影问题。 +- [x] **字体样式扩展**: 标题样式 4→12 个(+庞门正道/优设标题圆/阿里数黑体/文道潮黑/无界黑/厚底黑/寒蝉半圆体/欣意吉祥宋),字幕样式 4→8 个(+少女粉/清新绿/金色隶书/楷体红字)。 +- [x] **描边参数优化**: 所有预设 `stroke_size` 从 8 降至 4~5,配合原生描边视觉更干净。 +- [x] **TypeScript 类型修复**: Root.tsx `Composition` 泛型与 `calculateMetadata` 参数类型对齐;Video.tsx `VideoProps` 添加索引签名兼容 `Record`;VideoLayer.tsx 移除 `OffthreadVideo` 不支持的 `loop` prop。 +- [x] **进度条文案还原**: 进度条从显示后端推送消息改回固定 `正在AI生成中...`。 + +### Day 26: 前端优化:板块合并 + 序号标题 + UI 精细化 - [x] **板块合并**: 首页 9 个独立板块合并为 5 个主板块(配音方式+配音列表→三、配音;视频素材+时间轴→四、素材编辑;历史作品+作品预览→六、作品)。 - [x] **中文序号标题**: 一~十编号(首页一~六,发布页七~十),移除所有 emoji 图标。 - [x] **embedded 模式**: 6 个组件支持 `embedded` prop,嵌入时不渲染外层卡片/标题。 diff --git a/README.md b/README.md index aab92e2..ee1824c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ - 🎬 **高清唇形同步** - LatentSync 1.6 驱动,512×512 高分辨率 Latent Diffusion 模型。 - 🎙️ **多模态配音** - 支持 **EdgeTTS** (微软超自然语音, 10 语言) 和 **CosyVoice 3.0** (3秒极速声音克隆, 9语言+18方言, 语速可调)。上传参考音频自动 Whisper 转写 + 智能截取。配音前置工作流:先生成配音 → 选素材 → 生成视频。 - 📝 **智能字幕** - 集成 faster-whisper + Remotion,自动生成逐字高亮 (卡拉OK效果) 字幕。 -- 🎨 **样式预设** - 标题/副标题/字幕样式选择 + 预览 + 字号调节,支持自定义字体库。 +- 🎨 **样式预设** - 12 种标题 + 8 种字幕样式预设,支持预览 + 字号调节 + 自定义字体库。CSS 原生描边渲染,清晰无重影。 - 🏷️ **标题显示模式** - 片头标题支持 `短暂显示` / `常驻显示`,默认短暂显示(4秒),用户偏好自动持久化。 - 📌 **片头副标题** - 可选副标题显示在主标题下方,独立样式配置,AI 可同时生成,20 字限制。 - 🖼️ **作品预览一致性** - 标题/字幕预览与 Remotion 成片统一响应式缩放和自动换行,窄屏画布也稳定显示。 @@ -46,7 +46,7 @@ | 领域 | 核心技术 | 说明 | |------|----------|------| | **前端** | Next.js 16 | TypeScript, TailwindCSS, SWR, wavesurfer.js | -| **后端** | FastAPI | Python 3.10, AsyncIO, PM2 | +| **后端** | FastAPI | Python 3.12, AsyncIO, PM2 | | **数据库** | Supabase | PostgreSQL, Storage (本地/S3), Auth | | **唇形同步** | LatentSync 1.6 | PyTorch 2.5, Diffusers, DeepCache | | **声音克隆** | CosyVoice 3.0 | 0.5B 参数量,9 语言 + 18 方言 | @@ -62,7 +62,7 @@ ### 部署运维 - **[部署手册 (DEPLOY_MANUAL.md)](Docs/DEPLOY_MANUAL.md)** - 👈 **部署请看这里**!包含完整的环境搭建步骤。 - [参考音频服务部署 (COSYVOICE3_DEPLOY.md)](Docs/COSYVOICE3_DEPLOY.md) - 声音克隆模型部署指南。 -- [LatentSync 部署指南](models/LatentSync/DEPLOY.md) - 唇形同步模型独立部署。 +- [LatentSync 部署指南 (LATENTSYNC_DEPLOY.md)](Docs/LATENTSYNC_DEPLOY.md) - 唇形同步模型独立部署。 - [Supabase 部署指南 (SUPABASE_DEPLOY.md)](Docs/SUPABASE_DEPLOY.md) - Supabase 与认证系统配置。 - [支付宝部署指南 (ALIPAY_DEPLOY.md)](Docs/ALIPAY_DEPLOY.md) - 支付宝付费开通会员配置。 @@ -70,6 +70,8 @@ - [后端开发指南](Docs/BACKEND_README.md) - 接口规范与开发流程。 - [后端开发规范](Docs/BACKEND_DEV.md) - 分层约定与开发习惯。 - [前端开发指南](Docs/FRONTEND_DEV.md) - UI 组件与页面规范。 +- [前端组件文档](Docs/FRONTEND_README.md) - 组件结构与板块说明。 +- [Remotion 字幕部署 (SUBTITLE_DEPLOY.md)](Docs/SUBTITLE_DEPLOY.md) - 字幕渲染服务部署。 - [开发日志 (DevLogs)](Docs/DevLogs/) - 每日开发进度与技术决策记录。 --- diff --git a/backend/.env.example b/backend/.env.example index 30f3f49..2fe88c3 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -25,7 +25,7 @@ LATENTSYNC_USE_SERVER=true # LATENTSYNC_API_URL=http://localhost:8007 # 推理步数 (20-50, 越高质量越好,速度越慢) -LATENTSYNC_INFERENCE_STEPS=40 +LATENTSYNC_INFERENCE_STEPS=20 # 引导系数 (1.0-3.0, 越高唇同步越准,但可能抖动) LATENTSYNC_GUIDANCE_SCALE=2.0 diff --git a/backend/assets/styles/subtitle.json b/backend/assets/styles/subtitle.json index 82f17d4..563fb9b 100644 --- a/backend/assets/styles/subtitle.json +++ b/backend/assets/styles/subtitle.json @@ -54,5 +54,61 @@ "letter_spacing": 1, "bottom_margin": 72, "is_default": false + }, + { + "id": "subtitle_pink", + "label": "少女粉", + "font_file": "DingTalk JinBuTi.ttf", + "font_family": "DingTalkJinBuTi", + "font_size": 56, + "highlight_color": "#FF69B4", + "normal_color": "#FFFFFF", + "stroke_color": "#1A0010", + "stroke_size": 3, + "letter_spacing": 2, + "bottom_margin": 80, + "is_default": false + }, + { + "id": "subtitle_lime", + "label": "清新绿", + "font_file": "DingTalk Sans.ttf", + "font_family": "DingTalkSans", + "font_size": 50, + "highlight_color": "#76FF03", + "normal_color": "#FFFFFF", + "stroke_color": "#001A00", + "stroke_size": 3, + "letter_spacing": 1, + "bottom_margin": 78, + "is_default": false + }, + { + "id": "subtitle_gold", + "label": "金色隶书", + "font_file": "阿里妈妈刀隶体.ttf", + "font_family": "AliMamaDaoLiTi", + "font_size": 56, + "highlight_color": "#FDE68A", + "normal_color": "#E8D5B0", + "stroke_color": "#2B1B00", + "stroke_size": 3, + "letter_spacing": 3, + "bottom_margin": 80, + "is_default": false + }, + { + "id": "subtitle_kai", + "label": "楷体红字", + "font_file": "simkai.ttf", + "font_family": "SimKai", + "font_size": 54, + "highlight_color": "#FF4444", + "normal_color": "#FFFFFF", + "stroke_color": "#000000", + "stroke_size": 3, + "letter_spacing": 2, + "bottom_margin": 80, + "is_default": false } ] diff --git a/backend/assets/styles/title.json b/backend/assets/styles/title.json index dbafbcb..ab6d401 100644 --- a/backend/assets/styles/title.json +++ b/backend/assets/styles/title.json @@ -7,7 +7,7 @@ "font_size": 90, "color": "#FFFFFF", "stroke_color": "#000000", - "stroke_size": 8, + "stroke_size": 5, "letter_spacing": 5, "top_margin": 62, "font_weight": 900, @@ -21,7 +21,7 @@ "font_size": 72, "color": "#FFFFFF", "stroke_color": "#000000", - "stroke_size": 8, + "stroke_size": 5, "letter_spacing": 4, "top_margin": 60, "font_weight": 900, @@ -35,7 +35,7 @@ "font_size": 70, "color": "#FDE68A", "stroke_color": "#2B1B00", - "stroke_size": 8, + "stroke_size": 5, "letter_spacing": 3, "top_margin": 58, "font_weight": 800, @@ -49,10 +49,122 @@ "font_size": 72, "color": "#FFFFFF", "stroke_color": "#1F0A00", - "stroke_size": 8, + "stroke_size": 5, "letter_spacing": 4, "top_margin": 60, "font_weight": 900, "is_default": false + }, + { + "id": "title_pangmen", + "label": "庞门正道", + "font_file": "title/庞门正道标题体3.0.ttf", + "font_family": "PangMenZhengDao", + "font_size": 80, + "color": "#FFFFFF", + "stroke_color": "#000000", + "stroke_size": 5, + "letter_spacing": 5, + "top_margin": 60, + "font_weight": 900, + "is_default": false + }, + { + "id": "title_round", + "label": "优设标题圆", + "font_file": "title/优设标题圆.otf", + "font_family": "YouSheBiaoTiYuan", + "font_size": 78, + "color": "#FFFFFF", + "stroke_color": "#4A1A6B", + "stroke_size": 5, + "letter_spacing": 4, + "top_margin": 60, + "font_weight": 900, + "is_default": false + }, + { + "id": "title_alibaba", + "label": "阿里数黑体", + "font_file": "title/阿里巴巴数黑体.ttf", + "font_family": "AlibabaShuHeiTi", + "font_size": 72, + "color": "#FFFFFF", + "stroke_color": "#000000", + "stroke_size": 4, + "letter_spacing": 3, + "top_margin": 60, + "font_weight": 900, + "is_default": false + }, + { + "id": "title_chaohei", + "label": "文道潮黑", + "font_file": "title/文道潮黑.ttf", + "font_family": "WenDaoChaoHei", + "font_size": 76, + "color": "#00E5FF", + "stroke_color": "#001A33", + "stroke_size": 5, + "letter_spacing": 4, + "top_margin": 60, + "font_weight": 900, + "is_default": false + }, + { + "id": "title_wujie", + "label": "无界黑", + "font_file": "title/标小智无界黑.otf", + "font_family": "BiaoXiaoZhiWuJieHei", + "font_size": 74, + "color": "#FFFFFF", + "stroke_color": "#1A1A1A", + "stroke_size": 4, + "letter_spacing": 3, + "top_margin": 60, + "font_weight": 900, + "is_default": false + }, + { + "id": "title_houdi", + "label": "厚底黑", + "font_file": "title/Aa厚底黑.ttf", + "font_family": "AaHouDiHei", + "font_size": 76, + "color": "#FF6B6B", + "stroke_color": "#1A0000", + "stroke_size": 5, + "letter_spacing": 4, + "top_margin": 60, + "font_weight": 900, + "is_default": false + }, + { + "id": "title_banyuan", + "label": "寒蝉半圆体", + "font_file": "title/寒蝉半圆体.otf", + "font_family": "HanChanBanYuan", + "font_size": 78, + "color": "#FFFFFF", + "stroke_color": "#000000", + "stroke_size": 5, + "letter_spacing": 4, + "top_margin": 60, + "font_weight": 900, + "is_default": false + }, + { + "id": "title_jixiang", + "label": "欣意吉祥宋", + "font_file": "title/字体圈欣意吉祥宋.ttf", + "font_family": "XinYiJiXiangSong", + "font_size": 70, + "color": "#FDE68A", + "stroke_color": "#2B1B00", + "stroke_size": 5, + "letter_spacing": 3, + "top_margin": 58, + "font_weight": 800, + "is_default": false } ] diff --git a/remotion/src/Root.tsx b/remotion/src/Root.tsx index 91481fb..3c4b8a6 100644 --- a/remotion/src/Root.tsx +++ b/remotion/src/Root.tsx @@ -16,9 +16,9 @@ export const RemotionRoot: React.FC = () => { fps={25} width={1080} height={1920} - calculateMetadata={async ({ props }) => ({ - width: props.width || 1080, - height: props.height || 1920, + calculateMetadata={async ({ props }: { props: Record }) => ({ + width: (props.width as number) || 1080, + height: (props.height as number) || 1920, })} defaultProps={{ videoSrc: '', @@ -32,7 +32,7 @@ export const RemotionRoot: React.FC = () => { secondaryTitleStyle: undefined, width: 1080, height: 1920, - }} + } satisfies VideoProps} /> ); diff --git a/remotion/src/Video.tsx b/remotion/src/Video.tsx index d2cdb55..d6d2319 100644 --- a/remotion/src/Video.tsx +++ b/remotion/src/Video.tsx @@ -19,6 +19,7 @@ export interface VideoProps { secondaryTitleStyle?: TitleStyle; width?: number; height?: number; + [key: string]: unknown; } /** diff --git a/remotion/src/components/Subtitles.tsx b/remotion/src/components/Subtitles.tsx index 207c936..cd68773 100644 --- a/remotion/src/components/Subtitles.tsx +++ b/remotion/src/components/Subtitles.tsx @@ -38,16 +38,14 @@ const getFontFormat = (fontFile?: string) => { 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(','); -}; +/** + * 构建描边样式(使用 CSS 原生描边,避免 textShadow 重影) + */ +const buildStrokeStyle = (color: string, size: number): React.CSSProperties => ({ + WebkitTextStroke: `${size}px ${color}`, + paintOrder: 'stroke fill', + textShadow: `0 2px 4px rgba(0,0,0,0.4)`, +}); export const Subtitles: React.FC = ({ captions, style }) => { const frame = useCurrentFrame(); @@ -133,7 +131,7 @@ export const Subtitles: React.FC = ({ captions, style }) => { key={`${word.word}-${index}`} style={{ color: isHighlighted ? highlightColor : normalColor, - textShadow: buildTextShadow(strokeColor, strokeSize), + ...buildStrokeStyle(strokeColor, strokeSize), transition: 'color 0.05s ease', }} > diff --git a/remotion/src/components/Title.tsx b/remotion/src/components/Title.tsx index 5e3474a..b5ce6a6 100644 --- a/remotion/src/components/Title.tsx +++ b/remotion/src/components/Title.tsx @@ -44,16 +44,15 @@ const getFontFormat = (fontFile?: string) => { 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 * 2}px rgba(0,0,0,0.5)`, - `0 2px 4px rgba(0,0,0,0.3)` - ].join(','); -}; +/** + * 构建描边样式(使用 CSS 原生描边,避免 textShadow 重影) + * 返回需要合并到 style 对象上的属性 + */ +const buildStrokeStyle = (color: string, size: number): React.CSSProperties => ({ + WebkitTextStroke: `${size}px ${color}`, + paintOrder: 'stroke fill', + textShadow: `0 2px 4px rgba(0,0,0,0.3)`, +}); export const Title: React.FC = ({ title, @@ -194,7 +193,7 @@ export const Title: React.FC = ({ fontSize: `${fontSize}px`, fontWeight, fontFamily: fontFamilyCss, - textShadow: buildTextShadow(strokeColor, strokeSize), + ...buildStrokeStyle(strokeColor, strokeSize), margin: 0, width: '100%', boxSizing: 'border-box', @@ -217,7 +216,7 @@ export const Title: React.FC = ({ fontSize: `${stFontSize}px`, fontWeight: stFontWeight, fontFamily: stFontFile && stFontFile !== fontFile ? stFontFamilyCss : fontFamilyCss, - textShadow: buildTextShadow(stStrokeColor, stStrokeSize), + ...buildStrokeStyle(stStrokeColor, stStrokeSize), margin: 0, marginTop: `${stMarginTop}px`, width: '100%', diff --git a/remotion/src/components/VideoLayer.tsx b/remotion/src/components/VideoLayer.tsx index 65ba02b..b21dd57 100644 --- a/remotion/src/components/VideoLayer.tsx +++ b/remotion/src/components/VideoLayer.tsx @@ -21,7 +21,6 @@ export const VideoLayer: React.FC = ({