# 实现方案 — 视频帧显示链路修复

## 后端

### 1. `backend/schemas.py` — ProjectOut 增加 frame_count
```python
class ProjectOut(ProjectBase):
    id: int
    created_at: datetime
    updated_at: datetime
    frame_count: int = 0
```

### 2. `backend/routers/projects.py` — 三处修改
- `list_projects`: 为每个项目计算 `frame_count = len(p.frames)`（利用 ORM relationship）
- `list_frames`: 返回前将每个 `frame.image_url` 替换为 `get_presigned_url(frame.image_url, expires=3600)`
- `get_project`: 同样添加 `frame_count`

### 3. `backend/main.py` — lifespan 默认视频种子
启动时异步后台任务：
- 若数据库无项目且 `Data_MyVideo_1.mp4` 存在
- 创建 Project → 上传 MinIO → 调用 FFmpeg 解析帧 → 上传帧到 MinIO → 注册 Frame 记录 → 更新 status="ready"
- 使用 `asyncio.to_thread()` 避免阻塞事件循环

### 4. `backend/routers/media.py` — upload 自动创建项目
当 `project_id` 为空时：
- 以文件名创建 Project
- 将上传文件关联到该 project
- 返回中增加 `project_id`

## 前端

### 5. `src/lib/api.ts` — 新增两个 API
```ts
export async function getProjectFrames(projectId: string): Promise<Array<{ id: number; project_id: number; frame_index: number; image_url: string; width: number | null; height: number | null }>>
export async function parseMedia(projectId: string): Promise<{ project_id: number; frames_extracted: number; status: string; message: string }>
```

### 6. `src/components/VideoWorkspace.tsx` — 帧加载中枢
- 读取 `currentProject` 和 `frames`
- `useEffect` 监听 `currentProject.id`：
  - 调用 `getProjectFrames`
  - 映射后端字段到前端 `Frame[]` 结构写入 store
  - 若帧数为 0 且项目有 video_path，自动调用 `parseMedia` 触发解析，解析完成后重新获取
- 将当前帧 URL 通过 prop 传给 `CanvasArea`
- 将帧列表和索引控制传给 `FrameTimeline`
- 标题显示 `currentProject?.name`

### 7. `src/components/CanvasArea.tsx` — 真实帧渲染
- 新增 `frameUrl` prop
- `const [image] = useImage(frameUrl || '')`
- `runInference` 使用 `frameUrl` 替代硬编码 URL

### 8. `src/components/FrameTimeline.tsx` — 真实帧导航
- 从 store 读取 `frames` 和 `currentFrameIndex`
- `totalFrames = frames.length`
- 每个帧方块显示为 `<img>` 缩略图
- 点击调用 `setCurrentFrame(index)`
- 进度条范围 1..frames.length

### 9. `src/components/ProjectLibrary.tsx` — 完整上传链路
上传流程改为：
1. `createProject({ name: file.name })`
2. `uploadMedia(file, projectId)`
3. `parseMedia(projectId)`
4. `getProjects()` 刷新列表
