## 🔧 鉴权到期治理 + 多素材时间轴稳定性修复 (Day 24)

### 概述

本日主要完成两条主线：

1. **账号与鉴权治理**：会员到期改为请求时自动失效（登录/鉴权接口触发），并统一返回续费提示。
2. **视频生成稳定性**：围绕多素材时间轴、截取语义、拼接边界冻结、画面比例与字幕标题适配进行一轮端到端修复。

---

## 🔐 会员到期请求时失效 — 第一阶段 (Day 24)

### 目标

避免依赖定时任务，用户在触发登录或访问受保护接口时即可完成到期判定与账号停用。

### 行为调整

- 到期判断基于 `users.expires_at`。
- 判定到期后：
  - 将 `is_active` 自动置为 `false`
  - 删除该用户全部 session
  - 返回 `403`，提示：`会员已到期，请续费`

### 实现点

- `users.py` 新增 `deactivate_user_if_expired()`，并补充 `_parse_expires_at()` 统一时区解析。
- `deps.py` 在 `get_current_user` / `get_current_user_optional` 中统一接入到期检查。
- `auth/router.py` 在登录路径增加到期停用逻辑；`/api/auth/me` 统一走 `Depends(get_current_user)`。

---

## 🖼️ 画面比例控制 + 字幕标题适配 — 第二阶段 (Day 24)

### 2.1 输出画面比例可配置

- 时间轴顶部新增“画面比例”下拉：`9:16` / `16:9`。
- 默认值 `9:16`，并持久化到 localStorage。
- 生成请求携带 `output_aspect_ratio`，后端在单素材与多素材流程中统一按目标分辨率处理。

### 2.2 标题/字幕在窄屏画布防溢出

为减少“预览正常、成片溢出”的差异，统一了预览与渲染策略：

- 根据 composition 宽度进行响应式缩放。
- 开启可换行：`white-space: normal` + `word-break` + `overflow-wrap`。
- 描边、字距、上下边距同步按比例缩放。

### 2.3 片头标题显示模式（短暂/常驻）

- 在“标题与字幕”面板的“片头标题”行尾新增下拉，支持：`短暂显示` / `常驻显示`。
- 默认模式为 `短暂显示`，短暂模式默认时长为 4 秒。
- 用户选择会持久化到 localStorage，刷新后保持上次配置。
- 生成请求新增 `title_display_mode`，短暂模式透传 `title_duration=4.0`。
- Remotion 端到端支持该参数：
  - `short`：标题在设定时长后淡出并结束渲染；
  - `persistent`：标题全程常驻（保留淡入动画，不执行淡出）。

---

## 🎥 方向归一化 + 多素材拼接稳定性 — 第三阶段 (Day 24)

### 3.1 MOV 旋转元数据导致横竖识别错误

问题场景：编码分辨率是横屏，但依赖 rotation side-data 才能正确显示为竖屏（常见于手机 MOV）。

修复方案：

- `get_video_metadata()` 扩展返回 `rotation/effective_width/effective_height`。
- 新增 `normalize_orientation()`，在流程前对带旋转元数据素材做物理方向归一化。
- 单素材和多素材下载后统一执行方向归一化，再做分辨率决策。

### 3.2 多素材“只看到第一段”与边界冻结

针对拼接可靠性补了两类保护：

- **分配保护**：`custom_assignments` 与素材数量不一致时，后端回退自动分配，避免异常输入导致仅首段生效。
- **编码一致性**：
  - 片段准备阶段统一重编码；
  - concat 阶段不再走拷贝；
  - 进一步统一为 `25fps + CFR`，并在 concat 增加 `+genpts`，降低段边界时间基不连续导致的“画面冻结口型还动”风险。

---

## ⏱️ 时间轴截取语义对齐修复 — 第四阶段 (Day 24)

### 背景

时间轴设计语义是：

- 每段可以设置 `sourceStart/sourceEnd`；
- 总时长超出音频时，仅保留可见段，末段截齐音频；
- 总时长不足时，由最后可见段循环补齐。

本日将前后端对齐到这一语义。

### 4.1 `source_end` 全链路打通

此前仅传 `source_start`，导致后端无法准确知道“截到哪里”。

本次改动：

- 前端 `toCustomAssignments()` 增加可选 `source_end`。
- 后端 `CustomAssignment` schema 增加 `source_end`。
- workflow 将 `source_end` 透传到 `prepare_segment()`（单素材/多素材均支持）。
- `prepare_segment()` 增加 `source_end` 参数，按 `[source_start, source_end)` 计算可用片段，并在需要循环时先裁剪再循环，避免循环范围错位。

### 4.2 时间轴有效时长计算修复

修复 `sourceStart > 0 且 sourceEnd = 0` 时的有效时长错误：

- 旧逻辑会按整段素材时长计算；
- 新逻辑改为 `materialDuration - sourceStart`。

该修复同时用于：

- `recalcPositions()` 的段时长计算；
- TimelineEditor 中“循环补足”可视化比例计算。

### 4.3 可见段分配优先级修复

修复“可见段数 < 已选素材数时，custom_assignments 被丢弃回退自动分配”的问题：

- 生成请求优先以时间轴可见段的 `assignments` 为准；
- 超出时间轴的素材不参与本次生成。

### 4.4 单素材截取触发条件补齐

单素材模式下，若只改了终点（`sourceEnd > 0`）也会发送 `custom_assignments`，确保截取生效。

---

## 🧭 页面交互与体验细节 — 第五阶段 (Day 24)

- 页面刷新后自动回到顶部，避免从历史滚动位置进入页面。
- 素材列表与历史视频列表滚动增加“跳过首次自动滚动”保护，减少恢复状态时页面跳动。
- 时间轴比例区移除多余文案，保持信息简洁。

---

## 涉及文件汇总

### 后端修改

| 文件 | 变更 |
|------|------|
| `backend/app/repositories/users.py` | 新增 `deactivate_user_if_expired()` 与 `_parse_expires_at()` |
| `backend/app/core/deps.py` | `get_current_user` / `get_current_user_optional` 接入到期失效检查 |
| `backend/app/modules/auth/router.py` | 登录时到期停用 + `/api/auth/me` 统一鉴权依赖 |
| `backend/app/modules/videos/schemas.py` | `CustomAssignment` 新增 `source_end`；保留 `output_aspect_ratio` |
| `backend/app/modules/videos/workflow.py` | 多素材/单素材透传 `source_end`；多素材 prepare/concat 统一 25fps；标题显示模式参数透传 Remotion |
| `backend/app/services/video_service.py` | 旋转元数据解析与方向归一化；`prepare_segment` 支持 `source_end/target_fps`；concat 强制 CFR + `+genpts` |
| `backend/app/services/remotion_service.py` | render 支持 `title_display_mode/title_duration` 并传递到 render.ts |

### 前端修改

| 文件 | 变更 |
|------|------|
| `frontend/src/features/home/model/useTimelineEditor.ts` | `CustomAssignment` 新增 `source_end`；修复 sourceStart 开放终点时长计算 |
| `frontend/src/features/home/model/useHomeController.ts` | 多素材以可见 assignments 为准发送；单素材截取触发条件补齐 |
| `frontend/src/features/home/ui/TimelineEditor.tsx` | 画面比例下拉；循环比例按截取后有效时长计算 |
| `frontend/src/features/home/model/useHomePersistence.ts` | `outputAspectRatio` 与 `titleDisplayMode` 持久化 |
| `frontend/src/features/home/ui/HomePage.tsx` | 页面进入滚动到顶部；ClipTrimmer/Timeline 交互保持一致 |
| `frontend/src/features/home/ui/FloatingStylePreview.tsx` | 标题/字幕样式预览与成片渲染策略对齐 |
| `frontend/src/features/home/ui/TitleSubtitlePanel.tsx` | 标题行新增“短暂显示/常驻显示”下拉 |

### Remotion 修改

| 文件 | 变更 |
|------|------|
| `remotion/src/components/Title.tsx` | 标题响应式缩放与自动换行；新增短暂/常驻显示模式控制 |
| `remotion/src/components/Subtitles.tsx` | 字幕响应式缩放与自动换行，减少预览/成片差异 |
| `remotion/src/Video.tsx` | 新增 `titleDisplayMode` 透传到标题组件 |
| `remotion/src/Root.tsx` | 默认 props 增加 `titleDisplayMode='short'` 与 `titleDuration=4` |
| `remotion/render.ts` | CLI 参数新增 `--titleDisplayMode`，inputProps 增加 `titleDisplayMode` |

---

## 验证记录

- 后端语法检查：`python -m py_compile backend/app/modules/videos/schemas.py backend/app/modules/videos/workflow.py backend/app/services/video_service.py backend/app/services/remotion_service.py`
- 前端类型检查：`npx tsc --noEmit`
- 前端 ESLint：`npx eslint src/features/home/model/useHomeController.ts src/features/home/model/useHomePersistence.ts src/features/home/ui/HomePage.tsx src/features/home/ui/TitleSubtitlePanel.tsx`
- Remotion 渲染脚本构建：`npm run build:render`
