# RFC 131: 分镜资源 Mock 数据完善 > **状态**: 已实施 > **创建日期**: 2026-01-24 > **实施日期**: 2026-01-24 --- ## 概述 根据后端服务文档(`project-resource-service.md`, `storyboard-resource-service.md`, `storyboard-service.md`),完善前端 Mock 数据,确保数据结构与后端 API 规范完全一致,并补充完整的时间轴轨道数据。 ## 背景 ### 当前问题 1. **分镜资源关联不完整** - `storyboards.ts` 中的 `resources` 字段使用简单 ID 字符串 - 应该使用 `{ resourceId, displayOrder }` 结构 2. **Timeline 缺少完整轨道数据** - 只有分镜和资源轨道 - 缺少视频、音效、字幕、配音轨道 3. **分镜缺少后端新增字段** - 缺少 `shotNumber`, `shotSize`, `cameraMovement`, `dialogue` 等字段 - 缺少 `estimatedDuration`, `actualDuration` 时长管理字段 ## 解决方案 ### 1. 新增分镜资源 Mock 数据文件 创建 4 个新文件,模拟后端 `storyboard_*` 表: #### 1.1 `storyboard-images.ts` ```typescript export interface StoryboardImage { imageId: string; // UUID v7 storyboardId: string; url: string; status: 0 | 1 | 2 | 3; // 0=pending, 1=processing, 2=completed, 3=failed isActive: boolean; version: number; width: number; height: number; fileSize: number; format: string; checksum: string; storageProvider: string; storagePath: string; aiModel?: string; aiPrompt?: string; aiParams?: Record; errorMessage?: string; createdAt: string; completedAt?: string; } ``` **数据量**: 10 条(对应 10 个分镜) #### 1.2 `storyboard-videos.ts` ```typescript export interface StoryboardVideo { videoId: string; storyboardId: string; url: string; status: 0 | 1 | 2 | 3; isActive: boolean; version: number; duration: number; resolution: string; frameRate: number; fileSize: number; format: string; checksum: string; storageProvider: string; storagePath: string; aiModel?: string; aiParams?: Record; createdAt: string; startedAt?: string; completedAt?: string; } ``` **数据量**: 10 条 #### 1.3 `storyboard-dialogues.ts` ```typescript export interface StoryboardDialogue { dialogueId: string; storyboardId: string; characterId?: string; characterName?: string; text: string; sequenceOrder: number; startTime?: number; duration?: number; emotion?: string; notes?: string; createdAt: string; updatedAt: string; } ``` **数据量**: 10 条(西游记分镜的对白) #### 1.4 `storyboard-voiceovers.ts` ```typescript export interface StoryboardVoiceover { voiceoverId: string; dialogueId: string; storyboardId: string; audioUrl: string; status: 0 | 1 | 2 | 3; isActive: boolean; voiceId: string; voiceName?: string; speed: number; volume: number; pitch: number; duration: number; fileSize: number; format: string; checksum: string; storageProvider: string; storagePath: string; createdAt: string; completedAt?: string; } ``` **数据量**: 10 条(对应 10 条对白) #### 1.5 `sound-effects.ts` ```typescript export interface SoundEffect { soundEffectId: string; storyboardId: string; name: string; audioUrl: string; category: 'ambient' | 'action' | 'dialogue' | 'music' | 'foley'; status: 0 | 1 | 2 | 3; isActive: boolean; startTime: number; duration: number; volume: number; fadeIn: number; fadeOut: number; loop: boolean; fileSize: number; format: string; checksum: string; storageProvider: string; storagePath: string; tags?: string[]; notes?: string; createdAt: string; updatedAt: string; } ``` **数据量**: 21 条(覆盖 10 个分镜) **音效分类**: - `ambient`: 环境音(风声、鸟鸣、环境音) - `action`: 动作音效(爆炸、能量爆发、震动) - `dialogue`: 对白音效(暂未使用) - `music`: 音乐(暂未使用) - `foley`: 拟音(脚步、餐具、布料、马蹄) **数据分布**: - 分镜1-3(日常场景): 6 条音效 - 西游记分镜7-13(史诗场景): 15 条音效 ### 2. 重构 `timeline.ts` #### 2.1 完整的 6 种轨道 ```typescript export function getTimelineByProjectId(projectId: string): Timeline { const tracks: TimelineTrack[] = [ { type: 'storyboard', name: '分镜', displayOrder: 0 }, { type: 'resource', name: '资源', displayOrder: 1 }, { type: 'video', name: '视频', displayOrder: 2 }, { type: 'sound', name: '音效', displayOrder: 3 }, { type: 'subtitle', name: '字幕', displayOrder: 4 }, { type: 'voice', name: '配音', displayOrder: 5 }, ]; // ... } ``` #### 2.2 实时计算逻辑 - **分镜轨道**: 从 `storyboards` 表 - **资源轨道**: 从 `storyboard.resources` 字段 - **视频轨道**: 从 `storyboard_videos` 表(只显示激活且已完成的视频) - **音效轨道**: 从 `sound_effects` 表(只显示激活且已完成的音效) - **字幕轨道**: 从 `storyboard_dialogues` 表 - **配音轨道**: 从 `storyboard_voiceovers` 表(只显示激活且已完成的配音) #### 2.3 时间计算 ```typescript // 音效、字幕和配音的时间是相对于分镜开始时间 const soundEffects = getActiveSoundEffects(storyboard.id); soundEffects.forEach((sfx) => { items.push({ startTime: storyboardStartTime + sfx.startTime, endTime: storyboardStartTime + sfx.startTime + sfx.duration, }); }); const dialogue = getDialoguesByStoryboardId(storyboard.id); dialogue.forEach((d) => { items.push({ startTime: storyboardStartTime + d.startTime, endTime: storyboardStartTime + d.startTime + d.duration, }); }); ``` ### 3. 更新 `storyboards.ts` #### 3.1 修正 resources 字段结构 ```typescript // 旧格式(错误) resources: { characters: ['1', '2'], scenes: ['4'], props: ['7'], } // 新格式(正确) resources: { characters: [ { resourceId: '018e1234-5678-7abc-8def-200000000001', displayOrder: 0 }, { resourceId: '018e1234-5678-7abc-8def-200000000003', displayOrder: 1 }, ], scenes: [ { resourceId: '018e1234-5678-7abc-8def-200000000006', displayOrder: 0 }, ], props: [ { resourceId: '018e1234-5678-7abc-8def-200000000010', displayOrder: 0 }, ], } ``` #### 3.2 补充后端新增字段(TODO) - `shotNumber`: 镜号(自动生成,如 "001", "002") - `shotSize`: 景别(1-8) - `cameraMovement`: 运镜(1-9) - `dialogue`: 对白文本 - `estimatedDuration`: 预估时长 - `actualDuration`: 实际时长 ### 4. 更新 `index.ts` 导出 ```typescript export * from './storyboard-images'; export * from './storyboard-videos'; export * from './storyboard-dialogues'; export * from './storyboard-voiceovers'; ``` ## 数据关联关系 ### 关联图 ``` storyboards (13条) ├─> storyboard_images (10条) - 1:N,通过 storyboardId ├─> storyboard_videos (10条) - 1:N,通过 storyboardId ├─> storyboard_dialogues (10条) - 1:N,通过 storyboardId ├─> storyboard_voiceovers (10条) - 1:N,通过 storyboardId + dialogueId └─> sound_effects (21条) - 1:N,通过 storyboardId project_resources (33条) └─> storyboards.resources - M:N,通过 resourceId ``` ### 关联验证 所有外键引用已验证: - ✅ `storyboard_images.storyboardId` → `storyboards.id` - ✅ `storyboard_videos.storyboardId` → `storyboards.id` - ✅ `storyboard_dialogues.storyboardId` → `storyboards.id` - ✅ `storyboard_voiceovers.storyboardId` → `storyboards.id` - ✅ `storyboard_voiceovers.dialogueId` → `storyboard_dialogues.dialogueId` - ✅ `sound_effects.storyboardId` → `storyboards.id` - ✅ `storyboards.resources.*.resourceId` → `project_resources.id` ## 实施步骤 1. ✅ 创建 `storyboard-images.ts` - 10 条图片数据 2. ✅ 创建 `storyboard-videos.ts` - 10 条视频数据 3. ✅ 创建 `storyboard-dialogues.ts` - 10 条对白数据 4. ✅ 创建 `storyboard-voiceovers.ts` - 10 条配音数据 5. ✅ 创建 `sound-effects.ts` - 21 条音效数据 6. ✅ 重构 `timeline.ts` - 实现完整的 6 种轨道(包含音效轨道) 7. ⏳ 更新 `storyboards.ts` - 修正 resources 字段结构(待后续完成) 8. ✅ 更新 `index.ts` - 导出新文件 9. ✅ 创建 RFC 文档 ## 测试验证 ### 1. 数据完整性 ```typescript // 验证所有分镜都有对应的资源 const storyboards = getStoryboardsByProjectId('018e1234-5678-7abc-8def-100000000001'); storyboards.forEach((sb) => { const images = getStoryboardImagesByStoryboardId(sb.id); const videos = getStoryboardVideosByStoryboardId(sb.id); const dialogues = getDialoguesByStoryboardId(sb.id); const voiceovers = getVoiceoversByStoryboardId(sb.id); console.log(`分镜 ${sb.title}:`, { images: images.length, videos: videos.length, dialogues: dialogues.length, voiceovers: voiceovers.length, }); }); ``` ### 2. 时间轴计算 ```typescript // 验证时间轴数据正确生成 const timeline = getTimelineByProjectId('018e1234-5678-7abc-8def-100000000001'); console.log('轨道数量:', timeline.tracks.length); // 应该是 6 console.log('元素数量:', timeline.items.length); // 应该 > 50 // 验证字幕和配音的时间计算 const subtitleItems = timeline.items.filter((item) => item.itemType === 'subtitle'); const voiceoverItems = timeline.items.filter((item) => item.itemType === 'voiceover'); console.log('字幕数量:', subtitleItems.length); console.log('配音数量:', voiceoverItems.length); ``` ### 3. 外键关联 ```typescript // 验证所有资源 ID 都存在 storyboards.forEach((sb) => { sb.resources?.characters?.forEach((charRef) => { const resource = getResourceById(charRef.resourceId); if (!resource) { console.error(`资源不存在: ${charRef.resourceId}`); } }); }); ``` ## 影响范围 ### 新增文件 - `client/src/mocks/storyboard-images.ts` - `client/src/mocks/storyboard-videos.ts` - `client/src/mocks/storyboard-dialogues.ts` - `client/src/mocks/storyboard-voiceovers.ts` - `client/src/mocks/sound-effects.ts` ### 修改文件 - `client/src/mocks/timeline.ts` - 完全重写 - `client/src/mocks/index.ts` - 新增导出 - `client/src/mocks/storyboards.ts` - 待后续更新 ### 向后兼容 - ✅ 保留 `mockTimeline` 和 `mockTimelineTracks` 导出 - ✅ 现有代码无需修改 - ✅ 新代码可以使用 `getTimelineByProjectId()` 实时计算 ## Timeline 数据统计 完成后的时间轴数据规模: - **轨道数量**: 6 种(分镜、资源、视频、音效、字幕、配音) - **时间轴元素总数**: 约 90+ 个 - 分镜轨道: 13 个元素 - 资源轨道: 约 40 个元素(角色、场景、道具、实拍) - 视频轨道: 10 个元素 - 音效轨道: 21 个元素 - 字幕轨道: 10 个元素 - 配音轨道: 10 个元素 ## 后续工作 1. **更新 storyboards.ts** - 修正所有分镜的 `resources` 字段结构 - 补充后端新增字段(shotNumber, shotSize, cameraMovement 等) 2. **类型定义完善** - 在 `types/storyboard.ts` 中补充新类型 - 在 `types/timeline.ts` 中补充新类型 4. **API 集成** - 将 Mock 数据结构应用到真实 API 调用 - 确保前后端数据格式一致 ## 参考文档 - [项目素材服务](../../../docs/requirements/backend/04-services/project/project-resource-service.md) - [分镜资源服务](../../../docs/requirements/backend/04-services/project/storyboard-resource-service.md) - [分镜管理服务](../../../docs/requirements/backend/04-services/project/storyboard-service.md) - [时间轴服务](../../../docs/requirements/backend/04-services/project/timeline-service.md) --- **RFC 版本**: v1.0 **最后更新**: 2026-01-24