Files
ViGent2/Docs/DevLogs/Day14.md
Kevin Wong b2c1042c5c 更新
2026-02-04 18:04:17 +08:00

403 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Day 14 - 模型升级 + 标题标签生成 + 前端修复
**日期**2026-01-30
---
## 🚀 Qwen3-TTS 模型升级 (0.6B → 1.7B)
### 背景
为提升声音克隆质量,将 Qwen3-TTS 模型从 0.6B-Base 升级到 1.7B-Base。
### 变更内容
| 项目 | 升级前 | 升级后 |
|------|--------|--------|
| 模型 | 0.6B-Base | **1.7B-Base** |
| 大小 | 2.4GB | 6.8GB |
| 质量 | 基础 | 更高质量 |
### 代码修改
**文件**: `models/Qwen3-TTS/qwen_tts_server.py`
```python
# 升级前
MODEL_PATH = Path(__file__).parent / "checkpoints" / "0.6B-Base"
# 升级后
MODEL_PATH = Path(__file__).parent / "checkpoints" / "1.7B-Base"
```
### 模型下载
```bash
cd /home/rongye/ProgramFiles/ViGent2/models/Qwen3-TTS
# 下载 1.7B-Base 模型 (6.8GB)
modelscope download --model Qwen/Qwen3-TTS-12Hz-1.7B-Base --local_dir ./checkpoints/1.7B-Base
```
### 结果
- ✅ 模型加载正常 (GPU0, bfloat16)
- ✅ 声音克隆质量提升
- ✅ 推理速度可接受
---
## 🎨 标题和字幕显示优化
### 字幕组件优化 (`Subtitles.tsx`)
**文件**: `remotion/src/components/Subtitles.tsx`
优化内容:
- 调整高亮颜色配置
- 优化文字描边效果(多层阴影)
- 调整字间距和行高
```typescript
export const Subtitles: React.FC<SubtitlesProps> = ({
captions,
highlightColor = '#FFFF00', // 高亮颜色
normalColor = '#FFFFFF', // 普通文字颜色
fontSize = 52,
}) => {
// 样式优化
const style = {
textShadow: `
2px 2px 4px rgba(0,0,0,0.8),
-2px -2px 4px rgba(0,0,0,0.8),
...
`,
letterSpacing: '2px',
lineHeight: 1.4,
maxWidth: '90%',
};
};
```
### 标题组件优化 (`Title.tsx`)
**文件**: `remotion/src/components/Title.tsx`
优化内容:
- 淡入淡出动画效果
- 下滑入场动画
- 可配置显示时长
```typescript
interface TitleProps {
title: string;
duration?: number; // 标题显示时长默认3秒
fadeOutStart?: number; // 开始淡出的时间默认2秒
}
// 动画效果
// 淡入0-0.5 秒
// 淡出2-3 秒
// 下滑0-0.5 秒,-20px → 0px
```
### 结果
- ✅ 字幕显示更清晰
- ✅ 标题动画更流畅
---
## 🤖 标题标签自动生成功能
### 功能描述
使用 AI智谱 GLM-4-Flash根据口播文案自动生成视频标题和标签。
### 后端实现
#### 1. GLM 服务 (`glm_service.py`)
**文件**: `backend/app/services/glm_service.py`
```python
class GLMService:
"""智谱 GLM AI 服务"""
async def generate_meta(self, text: str) -> dict:
"""根据文案生成标题和标签"""
prompt = """根据以下口播文案生成一个吸引人的短视频标题和3个相关标签。
要求:
1. 标题要简洁有力能吸引观众点击不超过10个字
2. 标签要与内容相关便于搜索和推荐只要3个
返回格式:{"title": "标题", "tags": ["标签1", "标签2", "标签3"]}
"""
# 调用 GLM-4-Flash API
response = await self._call_api(prompt + text)
return self._parse_json(response)
```
**JSON 解析容错**
- 支持直接 JSON 解析
- 支持提取 JSON 块
- 支持 ```json 代码块提取
#### 2. API 端点 (`ai.py`)
**文件**: `backend/app/api/ai.py`
```python
from pydantic import BaseModel
class GenerateMetaRequest(BaseModel):
text: str # 口播文案
class GenerateMetaResponse(BaseModel):
title: str # 生成的标题
tags: list[str] # 生成的标签列表
@router.post("/generate-meta", response_model=GenerateMetaResponse)
async def generate_meta(request: GenerateMetaRequest):
"""AI 生成标题和标签"""
result = await glm_service.generate_meta(request.text)
return result
```
### 前端实现
**文件**: `frontend/src/app/page.tsx`
#### UI 按钮
```tsx
<button
onClick={handleGenerateMeta}
disabled={isGeneratingMeta || !text.trim()}
className="px-2 py-1 text-xs rounded transition-all whitespace-nowrap"
>
{isGeneratingMeta ? "⏳ 生成中..." : "🤖 AI生成标题标签"}
</button>
```
#### 处理逻辑
```typescript
const handleGenerateMeta = async () => {
if (!text.trim()) {
alert("请先输入口播文案");
return;
}
setIsGeneratingMeta(true);
try {
const { data } = await api.post('/api/ai/generate-meta', { text: text.trim() });
// 更新首页标题
setVideoTitle(data.title || "");
// 同步到发布页 localStorage
localStorage.setItem(`vigent_${storageKey}_publish_title`, data.title || "");
localStorage.setItem(`vigent_${storageKey}_publish_tags`, JSON.stringify(data.tags || []));
} catch (err: any) {
alert(`AI 生成失败: ${err.message}`);
} finally {
setIsGeneratingMeta(false);
}
};
```
### 发布页集成
**文件**: `frontend/src/app/publish/page.tsx`
从 localStorage 恢复 AI 生成的标题和标签:
```typescript
// 恢复标题和标签
const savedTitle = localStorage.getItem(`vigent_${storageKey}_publish_title`);
const savedTags = localStorage.getItem(`vigent_${storageKey}_publish_tags`);
if (savedTags) {
try {
const parsed = JSON.parse(savedTags);
if (Array.isArray(parsed)) {
setTags(parsed.join(', ')); // 数组转逗号分隔字符串
} else {
setTags(savedTags);
}
} catch {
setTags(savedTags);
}
}
```
### 结果
- ✅ AI 生成标题和标签功能正常
- ✅ 数据自动同步到发布页
- ✅ 支持 JSON 数组和字符串格式兼容
---
## 🐛 前端文本保存问题修复
### 问题描述
**现象**:页面刷新后,用户输入的文案、标题等数据丢失
**原因**
1. 认证状态恢复失败时,`userId``null`
2. 原代码判断 `!userId` 后用默认值覆盖 localStorage 数据
3. 导致已保存的用户数据被清空
### 解决方案
**文件**: `frontend/src/app/page.tsx`
#### 1. 添加恢复完成标志
```typescript
const [isRestored, setIsRestored] = useState(false);
```
#### 2. 等待认证完成后恢复数据
```typescript
useEffect(() => {
if (isAuthLoading) return; // 等待认证完成
// 使用 userId 或 'guest' 作为 key
const key = userId || 'guest';
// 从 localStorage 恢复数据
const savedText = localStorage.getItem(`vigent_${key}_text`);
if (savedText) setText(savedText);
// ... 恢复其他数据
setIsRestored(true); // 标记恢复完成
}, [userId, isAuthLoading]);
```
#### 3. 恢复完成后才保存
```typescript
useEffect(() => {
if (isRestored) {
localStorage.setItem(`vigent_${storageKey}_text`, text);
}
}, [text, storageKey, isRestored]);
```
### 用户隔离机制
```typescript
const storageKey = userId || 'guest';
```
| 用户状态 | storageKey | 说明 |
|----------|------------|------|
| 已登录 | `user_xxx` | 数据按用户隔离 |
| 未登录/认证失败 | `guest` | 使用统一 key |
### 数据恢复流程
```
1. 页面加载
2. 检查 isAuthLoading
├─ true: 等待认证完成
└─ false: 继续
3. 确定 storageKey (userId || 'guest')
4. 从 localStorage 读取数据
├─ 有保存数据: 恢复到状态
└─ 无保存数据: 使用默认值
5. 设置 isRestored = true
6. 后续状态变化时保存到 localStorage
```
### 保存的数据项
| Key | 说明 |
|-----|------|
| `vigent_${key}_text` | 口播文案 |
| `vigent_${key}_title` | 视频标题 |
| `vigent_${key}_subtitles` | 字幕开关 |
| `vigent_${key}_ttsMode` | TTS 模式 |
| `vigent_${key}_voice` | 选择的音色 |
| `vigent_${key}_material` | 选择的素材 |
| `vigent_${key}_publish_title` | 发布标题 |
| `vigent_${key}_publish_tags` | 发布标签 |
### 结果
- ✅ 页面刷新后数据正常恢复
- ✅ 认证失败时不会覆盖已保存数据
- ✅ 多用户数据隔离正常
---
## 🐛 登录页刷新循环修复
### 问题描述
**现象**:登录页未登录时不断刷新,无法停留在表单页面。
**原因**
1. `AuthProvider` 初始化时调用 `/api/auth/me`
2. 未登录返回 401
3. `axios` 全局拦截器遇到 401/403 重定向 `/login`
4. 登录页本身也在 Provider 中,导致循环刷新
### 解决方案
**文件**: `frontend/src/shared/api/axios.ts`
在拦截器中对公开路由跳过重定向,仅在受保护页面触发登录跳转:
```typescript
const PUBLIC_PATHS = new Set(['/login', '/register']);
const isPublicPath = typeof window !== 'undefined' && PUBLIC_PATHS.has(window.location.pathname);
if ((status === 401 || status === 403) && !isRedirecting && !isPublicPath) {
// ... 保持原有重定向逻辑
}
```
### 结果
- ✅ 登录页不再刷新,表单可正常输入
- ✅ 受保护页面仍会在 401/403 时跳转登录页
---
## 📁 今日修改文件清单
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `models/Qwen3-TTS/qwen_tts_server.py` | 修改 | 模型路径升级到 1.7B-Base |
| `Docs/QWEN3_TTS_DEPLOY.md` | 修改 | 更新部署文档为 1.7B 版本 |
| `remotion/src/components/Subtitles.tsx` | 修改 | 优化字幕显示效果 |
| `remotion/src/components/Title.tsx` | 修改 | 优化标题动画效果 |
| `backend/app/services/glm_service.py` | 新增 | GLM AI 服务 |
| `backend/app/api/ai.py` | 新增 | AI 生成标题标签 API |
| `backend/app/main.py` | 修改 | 注册 ai 路由 |
| `frontend/src/app/page.tsx` | 修改 | AI 生成按钮 + localStorage 修复 |
| `frontend/src/app/publish/page.tsx` | 修改 | 恢复 AI 生成的标签 |
| `frontend/src/shared/api/axios.ts` | 修改 | 公开路由跳过 401/403 登录重定向 |
---
## 🔗 相关文档
- [task_complete.md](../task_complete.md) - 任务总览
- [Day13.md](./Day13.md) - 声音克隆功能集成 + 字幕功能
- [QWEN3_TTS_DEPLOY.md](../QWEN3_TTS_DEPLOY.md) - Qwen3-TTS 1.7B 部署指南