# 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 部署指南
