remotion-claude-agent-demo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +160 -0
  2. package/apps/web/README.md +36 -0
  3. package/apps/web/env.example +20 -0
  4. package/apps/web/eslint.config.mjs +18 -0
  5. package/apps/web/next.config.ts +7 -0
  6. package/apps/web/package-lock.json +10348 -0
  7. package/apps/web/package.json +35 -0
  8. package/apps/web/postcss.config.mjs +7 -0
  9. package/apps/web/public/file.svg +1 -0
  10. package/apps/web/public/globe.svg +1 -0
  11. package/apps/web/public/next.svg +1 -0
  12. package/apps/web/public/vercel.svg +1 -0
  13. package/apps/web/public/window.svg +1 -0
  14. package/apps/web/src/app/.well-known/agent-card.json/route.ts +50 -0
  15. package/apps/web/src/app/background-tasks/[jobId]/cancel/route.ts +29 -0
  16. package/apps/web/src/app/events/stream/route.ts +58 -0
  17. package/apps/web/src/app/favicon.ico +0 -0
  18. package/apps/web/src/app/globals.css +174 -0
  19. package/apps/web/src/app/layout.tsx +34 -0
  20. package/apps/web/src/app/messages/answer/route.ts +57 -0
  21. package/apps/web/src/app/messages/stream/route.ts +381 -0
  22. package/apps/web/src/app/page.tsx +358 -0
  23. package/apps/web/src/app/tasks/[taskId]/cancel/route.ts +24 -0
  24. package/apps/web/src/app/tasks/[taskId]/route.ts +24 -0
  25. package/apps/web/src/app/tasks/route.ts +13 -0
  26. package/apps/web/src/components/chat/agent-blocks.tsx +111 -0
  27. package/apps/web/src/components/chat/ask-user-question-panel.tsx +172 -0
  28. package/apps/web/src/components/chat/session-sidebar.tsx +222 -0
  29. package/apps/web/src/components/chat/subagent-activity-sidebar.tsx +248 -0
  30. package/apps/web/src/components/chat/tool-blocks.tsx +550 -0
  31. package/apps/web/src/lib/a2a/activity-store.ts +150 -0
  32. package/apps/web/src/lib/a2a/client.ts +357 -0
  33. package/apps/web/src/lib/a2a/sse.ts +19 -0
  34. package/apps/web/src/lib/a2a/task-store.ts +111 -0
  35. package/apps/web/src/lib/a2a/types.ts +216 -0
  36. package/apps/web/src/lib/agent/answer-store.ts +109 -0
  37. package/apps/web/src/lib/agent/background-delivery.ts +343 -0
  38. package/apps/web/src/lib/agent/background-tool.ts +78 -0
  39. package/apps/web/src/lib/agent/background.ts +452 -0
  40. package/apps/web/src/lib/agent/chat.ts +543 -0
  41. package/apps/web/src/lib/agent/session-store.ts +26 -0
  42. package/apps/web/src/lib/chat/types.ts +44 -0
  43. package/apps/web/src/lib/env.ts +31 -0
  44. package/apps/web/src/lib/hooks/useA2AChat.ts +863 -0
  45. package/apps/web/src/lib/state/chat-atoms.ts +52 -0
  46. package/apps/web/src/lib/workspace.ts +9 -0
  47. package/apps/web/tsconfig.json +35 -0
  48. package/bin/remotion-agent.js +451 -0
  49. package/package.json +34 -0
  50. package/templates/.claude/CLAUDE.md +95 -0
  51. package/templates/.claude/README.md +129 -0
  52. package/templates/.claude/agents/composer-agent.md +188 -0
  53. package/templates/.claude/agents/crafter.md +181 -0
  54. package/templates/.claude/agents/creator.md +134 -0
  55. package/templates/.claude/agents/perceiver.md +92 -0
  56. package/templates/.claude/settings.json +36 -0
  57. package/templates/.claude/settings.local.json +39 -0
  58. package/templates/.claude/skills/agent-browser/SKILL.md +349 -0
  59. package/templates/.claude/skills/agent-browser/references/authentication.md +188 -0
  60. package/templates/.claude/skills/agent-browser/references/proxy-support.md +175 -0
  61. package/templates/.claude/skills/agent-browser/references/session-management.md +181 -0
  62. package/templates/.claude/skills/agent-browser/references/snapshot-refs.md +186 -0
  63. package/templates/.claude/skills/agent-browser/references/video-recording.md +162 -0
  64. package/templates/.claude/skills/agent-browser/templates/authenticated-session.sh +91 -0
  65. package/templates/.claude/skills/agent-browser/templates/capture-workflow.sh +68 -0
  66. package/templates/.claude/skills/agent-browser/templates/form-automation.sh +64 -0
  67. package/templates/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
  68. package/templates/.claude/skills/algorithmic-art/SKILL.md +405 -0
  69. package/templates/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
  70. package/templates/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
  71. package/templates/.claude/skills/asset-validator/SKILL.md +376 -0
  72. package/templates/.claude/skills/audio-video-sync/SKILL.md +219 -0
  73. package/templates/.claude/skills/bgm-manager/SKILL.md +334 -0
  74. package/templates/.claude/skills/remotion-best-practices/SKILL.md +45 -0
  75. package/templates/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
  76. package/templates/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
  77. package/templates/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
  78. package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
  79. package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
  80. package/templates/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
  81. package/templates/.claude/skills/remotion-best-practices/rules/audio.md +172 -0
  82. package/templates/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
  83. package/templates/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
  84. package/templates/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
  85. package/templates/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
  86. package/templates/.claude/skills/remotion-best-practices/rules/display-captions.md +126 -0
  87. package/templates/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
  88. package/templates/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
  89. package/templates/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
  90. package/templates/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
  91. package/templates/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
  92. package/templates/.claude/skills/remotion-best-practices/rules/gifs.md +138 -0
  93. package/templates/.claude/skills/remotion-best-practices/rules/images.md +130 -0
  94. package/templates/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
  95. package/templates/.claude/skills/remotion-best-practices/rules/lottie.md +68 -0
  96. package/templates/.claude/skills/remotion-best-practices/rules/maps.md +403 -0
  97. package/templates/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
  98. package/templates/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
  99. package/templates/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
  100. package/templates/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
  101. package/templates/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
  102. package/templates/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
  103. package/templates/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
  104. package/templates/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
  105. package/templates/.claude/skills/remotion-best-practices/rules/transitions.md +122 -0
  106. package/templates/.claude/skills/remotion-best-practices/rules/trimming.md +53 -0
  107. package/templates/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
  108. package/templates/.claude/skills/remotion-components/SKILL.md +453 -0
  109. package/templates/.claude/skills/render-config/SKILL.md +290 -0
  110. package/templates/.claude/skills/script-writer/SKILL.md +59 -0
  111. package/templates/.claude/skills/style-director/script-writer/SKILL.md +82 -0
  112. package/templates/.claude/skills/style-director/style-director/SKILL.md +287 -0
  113. package/templates/.claude/skills/style-director/style-director/references/audience-and-scenarios.md +43 -0
  114. package/templates/.claude/skills/style-director/style-director/references/interaction-innovation.md +26 -0
  115. package/templates/.claude/skills/style-director/style-director/references/motion-grammar.md +66 -0
  116. package/templates/.claude/skills/style-director/style-director/references/quality-checklist.md +29 -0
  117. package/templates/.claude/skills/style-director/style-director/references/scene-recipes.md +38 -0
  118. package/templates/.claude/skills/style-director/style-director/references/visual-style-system.md +148 -0
  119. package/templates/.claude/skills/subtitle-composer/SKILL.md +304 -0
  120. package/templates/.claude/skills/subtitle-processor/SKILL.md +308 -0
  121. package/templates/.claude/skills/timeline-generator/SKILL.md +253 -0
  122. package/templates/.claude/skills/video-preflight-check/SKILL.md +353 -0
  123. package/templates/.claude/skills/voice-synthesizer/SKILL.md +296 -0
  124. package/templates/.claude/skills/voice-synthesizer/scripts/synthesize_voice.py +315 -0
  125. package/templates/.claude/skills/voice-synthesizer/scripts/tts_cli.py +142 -0
  126. package/templates/.claude/skills/web-design-guidelines/SKILL.md +36 -0
  127. package/templates/.claude/skills/youtube-downloader/SKILL.md +99 -0
  128. package/templates/.claude/skills/youtube-downloader/scripts/download_video.py +145 -0
@@ -0,0 +1,308 @@
1
+ ---
2
+ name: subtitle-processor
3
+ version: 1.0.0
4
+ description: 字幕处理技能。从VTT文件自动生成Remotion字幕数据,智能分割长句,确保可读性。
5
+ triggers:
6
+ - 字幕处理
7
+ - subtitle
8
+ - VTT解析
9
+ - 字幕分割
10
+ tools:
11
+ - Read
12
+ - Write
13
+ - Bash
14
+ ---
15
+
16
+ # 字幕处理技能 (Subtitle Processor)
17
+
18
+ 从语音合成生成的 VTT 文件自动提取字幕数据,智能分割过长字幕,生成 Remotion 可用的字幕配置。
19
+
20
+ ## 核心原理
21
+
22
+ **问题**:
23
+ 1. VTT 文件需要手动转换为 Remotion 字幕格式
24
+ 2. 语音合成生成的字幕可能一句话很长(30+ 字),显示效果差
25
+ 3. 时间戳是秒和毫秒,需要转换为帧数
26
+
27
+ **解决**:
28
+ 1. 自动解析 VTT 提取文本和时间
29
+ 2. 智能分割长句(按标点符号自然断句)
30
+ 3. 转换时间戳为帧数供 Remotion 使用
31
+
32
+ ## VTT 解析
33
+
34
+ ```typescript
35
+ interface SubtitleCue {
36
+ start: number; // 开始帧
37
+ end: number; // 结束帧
38
+ text: string; // 字幕文本
39
+ }
40
+
41
+ function parseVTT(vttContent: string, fps: number = 30): SubtitleCue[] {
42
+ const cues: SubtitleCue[] = [];
43
+ const lines = vttContent.trim().split('\n');
44
+
45
+ let i = 0;
46
+ while (i < lines.length) {
47
+ const line = lines[i].trim();
48
+
49
+ // 匹配时间戳行: 00:00:00.000 --> 00:00:02.500
50
+ if (line.includes('-->')) {
51
+ const [startStr, endStr] = line.split('-->').map(s => s.trim());
52
+ const start = parseVttTime(startStr, fps);
53
+ const end = parseVttTime(endStr, fps);
54
+
55
+ // 获取字幕文本(可能多行)
56
+ i++;
57
+ let text = '';
58
+ while (i < lines.length && lines[i].trim() && !lines[i].match(/^\d+$/)) {
59
+ text += (text ? ' ' : '') + lines[i].trim();
60
+ i++;
61
+ }
62
+
63
+ if (text) {
64
+ cues.push({ start, end, text });
65
+ }
66
+ } else {
67
+ i++;
68
+ }
69
+ }
70
+
71
+ return cues;
72
+ }
73
+
74
+ // 时间戳转帧数: 00:00:05.187 → 156 frames (at 30fps)
75
+ function parseVttTime(timeStr: string, fps: number): number {
76
+ const parts = timeStr.split(':');
77
+ const seconds =
78
+ parseFloat(parts[0]) * 3600 +
79
+ parseFloat(parts[1]) * 60 +
80
+ parseFloat(parts[2].replace(',', '.'));
81
+ return Math.round(seconds * fps);
82
+ }
83
+ ```
84
+
85
+ ## 智能长句分割
86
+
87
+ ```typescript
88
+ const MAX_CHARS = 25; // 中文最大字符数(英文约50)
89
+
90
+ function splitLongSubtitle(cue: SubtitleCue): SubtitleCue[] {
91
+ const { start, end, text } = cue;
92
+
93
+ // 长度合理,直接返回
94
+ if (text.length <= MAX_CHARS) {
95
+ return [cue];
96
+ }
97
+
98
+ // 优先在标点符号处分割
99
+ const splitPoints = ['。', '!', '?', ',', '、', ';', ' '];
100
+
101
+ const segments: string[] = [];
102
+ let remaining = text;
103
+
104
+ while (remaining.length > MAX_CHARS) {
105
+ let splitIndex = -1;
106
+
107
+ // 在前 MAX_CHARS 字符中找分割点
108
+ for (const point of splitPoints) {
109
+ const idx = remaining.lastIndexOf(point, MAX_CHARS);
110
+ if (idx > 0) {
111
+ splitIndex = idx + 1;
112
+ break;
113
+ }
114
+ }
115
+
116
+ // 没找到分割点,强制在 MAX_CHARS 处分割
117
+ if (splitIndex === -1) {
118
+ splitIndex = MAX_CHARS;
119
+ }
120
+
121
+ segments.push(remaining.slice(0, splitIndex).trim());
122
+ remaining = remaining.slice(splitIndex).trim();
123
+ }
124
+
125
+ if (remaining) {
126
+ segments.push(remaining);
127
+ }
128
+
129
+ // 按比例分配时间
130
+ const totalDuration = end - start;
131
+ const totalChars = segments.reduce((sum, s) => sum + s.length, 0);
132
+
133
+ const result: SubtitleCue[] = [];
134
+ let currentStart = start;
135
+
136
+ for (const segment of segments) {
137
+ const duration = Math.round((segment.length / totalChars) * totalDuration);
138
+ result.push({
139
+ start: currentStart,
140
+ end: currentStart + duration,
141
+ text: segment,
142
+ });
143
+ currentStart += duration;
144
+ }
145
+
146
+ return result;
147
+ }
148
+ ```
149
+
150
+ ## 生成 Remotion 字幕数据
151
+
152
+ ```typescript
153
+ function generateSubtitleData(vttFiles: string[], fps: number, outputPath: string): void {
154
+ const subtitles: Record<string, SubtitleCue[]> = {};
155
+
156
+ for (const vttFile of vttFiles) {
157
+ const sceneName = extractSceneName(vttFile);
158
+ const vttContent = fs.readFileSync(vttFile, 'utf-8');
159
+
160
+ // 解析 VTT
161
+ const cues = parseVTT(vttContent, fps);
162
+
163
+ // 分割长句
164
+ const processedCues = cues.flatMap(splitLongSubtitle);
165
+
166
+ subtitles[sceneName] = processedCues;
167
+ }
168
+
169
+ // 生成 TypeScript 文件
170
+ const content = `// Auto-generated by subtitle-processor
171
+ // DO NOT EDIT MANUALLY - This file is regenerated on each build
172
+
173
+ export interface SubtitleCue {
174
+ start: number;
175
+ end: number;
176
+ text: string;
177
+ }
178
+
179
+ export const SUBTITLES: Record<string, SubtitleCue[]> = ${JSON.stringify(subtitles, null, 2)};
180
+ `;
181
+
182
+ fs.writeFileSync(outputPath, content, 'utf-8');
183
+
184
+ const totalCues = Object.values(subtitles).reduce((sum, arr) => sum + arr.length, 0);
185
+ console.log(`✅ Subtitle data generated: ${Object.keys(subtitles).length} scenes, ${totalCues} cues`);
186
+ }
187
+ ```
188
+
189
+ ## 使用方法
190
+
191
+ ### 在构建脚本中调用
192
+
193
+ 创建 `scripts/generate-subtitles.ts`:
194
+ ```typescript
195
+ import * as fs from 'fs';
196
+ import * as path from 'path';
197
+
198
+ const VOICES_DIR = path.join(__dirname, '../public/voices');
199
+ const OUTPUT_PATH = path.join(__dirname, '../src/subtitle-data.ts');
200
+ const FPS = 30;
201
+
202
+ // 读取所有 VTT 文件
203
+ const vttFiles = fs.readdirSync(VOICES_DIR)
204
+ .filter(f => f.endsWith('.vtt'))
205
+ .sort()
206
+ .map(f => path.join(VOICES_DIR, f));
207
+
208
+ // 生成字幕数据
209
+ generateSubtitleData(vttFiles, FPS, OUTPUT_PATH);
210
+ ```
211
+
212
+ ### 在 Remotion 组件中使用
213
+
214
+ ```tsx
215
+ import { SUBTITLES } from './subtitle-data';
216
+ import type { SubtitleCue } from './subtitle-data';
217
+
218
+ // Subtitles.tsx
219
+ interface SubtitlesProps {
220
+ cues: SubtitleCue[];
221
+ // 其他自定义样式属性...
222
+ }
223
+
224
+ export const Subtitles: React.FC<SubtitlesProps> = ({ cues }) => {
225
+ const frame = useCurrentFrame();
226
+
227
+ const currentCue = cues.find(
228
+ (cue) => frame >= cue.start && frame <= cue.end
229
+ );
230
+
231
+ if (!currentCue) return null;
232
+
233
+ // 自定义动画和样式...
234
+ return <div>{currentCue.text}</div>;
235
+ };
236
+
237
+ // MainVideo.tsx
238
+ <TransitionSeries.Sequence durationInFrames={SCENES.hook.duration}>
239
+ <HookScene />
240
+ <Audio src={staticFile("voices/seg_01_hook.mp3")} />
241
+ <Subtitles cues={SUBTITLES.hook} />
242
+ </TransitionSeries.Sequence>
243
+ ```
244
+
245
+ ## 字幕分割示例
246
+
247
+ **原始 VTT**:
248
+ ```vtt
249
+ 00:00:00.000 --> 00:00:05.187
250
+ 传统的AI开发中,你需要手动实现工具调用循环,处理复杂的状态管理。
251
+ ```
252
+
253
+ **分割后**:
254
+ ```typescript
255
+ [
256
+ {
257
+ start: 0,
258
+ end: 90,
259
+ text: "传统的AI开发中"
260
+ },
261
+ {
262
+ start: 90,
263
+ end: 156,
264
+ text: "你需要手动实现工具调用循环,处理复杂的状态管理。"
265
+ }
266
+ ]
267
+ ```
268
+
269
+ ## 输出示例
270
+
271
+ ```
272
+ ✅ Subtitle data generated: 9 scenes, 24 cues
273
+
274
+ Subtitle statistics:
275
+ hook : 2 cues (avg: 19.5 chars)
276
+ intro : 4 cues (avg: 21.3 chars)
277
+ what : 4 cues (avg: 23.8 chars)
278
+ demo : 5 cues (avg: 20.1 chars)
279
+ tools : 5 cues (avg: 18.6 chars)
280
+ features : 4 cues (avg: 22.4 chars)
281
+ subagent : 4 cues (avg: 19.8 chars)
282
+ install : 4 cues (avg: 16.2 chars)
283
+ cta : 3 cues (avg: 21.7 chars)
284
+ ```
285
+
286
+ ## 优势
287
+
288
+ ✅ **自动化**: 不再手动编写字幕数据
289
+ ✅ **可读性**: 智能分割长句,避免字幕过长
290
+ ✅ **精确对齐**: 基于 VTT 毫秒级时间戳
291
+ ✅ **自然断句**: 优先在标点符号处分割
292
+ ✅ **类型安全**: 生成 TypeScript 接口
293
+
294
+ ## 配置选项
295
+
296
+ 可在脚本中调整的参数:
297
+ ```typescript
298
+ const MAX_CHARS = 25; // 最大字符数
299
+ const SPLIT_POINTS = [...]; // 分割优先级
300
+ const MIN_DURATION = 45; // 最短显示时间(帧)
301
+ ```
302
+
303
+ ## 注意事项
304
+
305
+ - 分割后的字幕时长按字符数比例分配
306
+ - 如果字幕仍然过长,会强制在 MAX_CHARS 处截断
307
+ - 对于英文,建议 MAX_CHARS 设为 50
308
+ - 生成的配置文件应添加到 `.gitignore`
@@ -0,0 +1,253 @@
1
+ ---
2
+ name: timeline-generator
3
+ version: 1.0.0
4
+ description: 自动时间线生成技能。从VTT文件自动计算精确的场景时长和帧数,消除手动硬编码。
5
+ triggers:
6
+ - 时间线生成
7
+ - timeline
8
+ - 音频时长
9
+ - 场景时间
10
+ tools:
11
+ - Read
12
+ - Write
13
+ - Bash
14
+ ---
15
+
16
+ # 时间线生成技能 (Timeline Generator)
17
+
18
+ 从语音合成生成的 VTT 字幕文件自动计算每个场景的精确时长,生成 TypeScript 配置文件供 Remotion 项目使用。
19
+
20
+ ## 核心原理
21
+
22
+ **问题**: 手动填写场景时长容易出错,配音重新生成后时长变化导致不同步。
23
+
24
+ **解决**: 从 VTT 文件解析音频结束时间,自动计算场景时长 = 音频时长 + 缓冲时间。
25
+
26
+ ## VTT 时间戳解析
27
+
28
+ VTT 文件格式示例:
29
+ ```vtt
30
+ WEBVTT
31
+
32
+ 00:00:00.000 --> 00:00:02.500
33
+ 第一句话
34
+
35
+ 00:00:02.500 --> 00:00:05.187
36
+ 第二句话
37
+
38
+ 00:00:05.187 --> 00:00:08.862
39
+ 第三句话
40
+ ```
41
+
42
+ **提取最大结束时间作为音频时长:**
43
+ ```typescript
44
+ function getAudioDuration(vttPath: string): number {
45
+ const content = fs.readFileSync(vttPath, 'utf-8');
46
+ let maxEndTime = 0;
47
+
48
+ // 匹配时间戳: HH:MM:SS.mmm --> HH:MM:SS.mmm
49
+ const timeRegex = /(\d{2}):(\d{2}):(\d{2})[,\.](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[,\.](\d{3})/g;
50
+
51
+ let match;
52
+ while ((match = timeRegex.exec(content)) !== null) {
53
+ const endTime =
54
+ parseInt(match[5]) * 3600 +
55
+ parseInt(match[6]) * 60 +
56
+ parseInt(match[7]) +
57
+ parseInt(match[8]) / 1000;
58
+ maxEndTime = Math.max(maxEndTime, endTime);
59
+ }
60
+
61
+ return maxEndTime;
62
+ }
63
+ ```
64
+
65
+ ## 场景时长计算
66
+
67
+ ```typescript
68
+ interface SceneTiming {
69
+ name: string;
70
+ audioDuration: number; // 实际音频时长(秒)
71
+ start: number; // 开始帧
72
+ duration: number; // 场景总时长(帧)
73
+ }
74
+
75
+ const FPS = 30;
76
+ const BUFFER = 0.5; // 音频后缓冲(秒)
77
+ const TRANSITION = 10; // 转场重叠(帧)
78
+
79
+ function calculateSceneTimings(vttFiles: string[]): SceneTiming[] {
80
+ const timings: SceneTiming[] = [];
81
+ let currentFrame = 0;
82
+
83
+ vttFiles.forEach((vttFile, index) => {
84
+ const sceneName = extractSceneName(vttFile);
85
+ const audioDuration = getAudioDuration(vttFile);
86
+
87
+ // 场景时长 = (音频时长 + 缓冲) × FPS + 转场重叠
88
+ const sceneDuration = Math.ceil((audioDuration + BUFFER) * FPS) + TRANSITION;
89
+
90
+ timings.push({
91
+ name: sceneName,
92
+ audioDuration: audioDuration,
93
+ start: currentFrame,
94
+ duration: sceneDuration,
95
+ });
96
+
97
+ // 下一场景开始位置(减去转场重叠)
98
+ const isLast = index === vttFiles.length - 1;
99
+ currentFrame += Math.ceil((audioDuration + BUFFER) * FPS) - (isLast ? 0 : TRANSITION);
100
+ });
101
+
102
+ return timings;
103
+ }
104
+ ```
105
+
106
+ ## 生成 TypeScript 配置
107
+
108
+ ```typescript
109
+ function generateTimelineConfig(timings: SceneTiming[], outputPath: string): void {
110
+ // 提取音频时长对象
111
+ const audioDurations = timings.reduce((acc, t) => {
112
+ acc[t.name] = parseFloat(t.audioDuration.toFixed(2));
113
+ return acc;
114
+ }, {} as Record<string, number>);
115
+
116
+ // 提取场景配置
117
+ const scenes = timings.reduce((acc, t) => {
118
+ acc[t.name] = {
119
+ start: t.start,
120
+ duration: t.duration,
121
+ };
122
+ return acc;
123
+ }, {} as Record<string, {start: number; duration: number}>);
124
+
125
+ // 计算总时长
126
+ const totalDuration = Math.max(...timings.map(t => t.start + t.duration));
127
+
128
+ // 生成 TypeScript 文件
129
+ const content = `// Auto-generated by timeline-generator
130
+ // DO NOT EDIT MANUALLY - This file is regenerated on each build
131
+
132
+ export const AUDIO_DURATIONS = ${JSON.stringify(audioDurations, null, 2)};
133
+
134
+ export const SCENES = ${JSON.stringify(scenes, null, 2)};
135
+
136
+ export const TOTAL_DURATION = ${totalDuration};
137
+
138
+ export const FPS = 30;
139
+ `;
140
+
141
+ fs.writeFileSync(outputPath, content, 'utf-8');
142
+ console.log(`✅ Timeline config generated: ${timings.length} scenes, ${totalDuration} frames (${(totalDuration / 30).toFixed(1)}s)`);
143
+ }
144
+ ```
145
+
146
+ ## 使用方法
147
+
148
+ ### 在构建脚本中调用
149
+
150
+ 创建 `scripts/generate-timeline.ts`:
151
+ ```typescript
152
+ import * as fs from 'fs';
153
+ import * as path from 'path';
154
+
155
+ const VOICES_DIR = path.join(__dirname, '../public/voices');
156
+ const OUTPUT_PATH = path.join(__dirname, '../src/timeline-config.ts');
157
+
158
+ // 读取所有 VTT 文件
159
+ const vttFiles = fs.readdirSync(VOICES_DIR)
160
+ .filter(f => f.endsWith('.vtt'))
161
+ .sort()
162
+ .map(f => path.join(VOICES_DIR, f));
163
+
164
+ // 计算时间线
165
+ const timings = calculateSceneTimings(vttFiles);
166
+
167
+ // 生成配置文件
168
+ generateTimelineConfig(timings, OUTPUT_PATH);
169
+ ```
170
+
171
+ ### 在 package.json 中配置
172
+ ```json
173
+ {
174
+ "scripts": {
175
+ "generate:timeline": "tsx scripts/generate-timeline.ts",
176
+ "prebuild": "npm run generate:timeline",
177
+ "preview": "npm run generate:timeline && remotion preview",
178
+ "render": "npm run generate:timeline && remotion render"
179
+ }
180
+ }
181
+ ```
182
+
183
+ ### 在 Remotion 组件中使用
184
+
185
+ ```tsx
186
+ import { SCENES, AUDIO_DURATIONS, TOTAL_DURATION } from './timeline-config';
187
+
188
+ // MainVideo.tsx
189
+ export const MainVideo: React.FC = () => {
190
+ return (
191
+ <TransitionSeries>
192
+ <TransitionSeries.Sequence durationInFrames={SCENES.hook.duration}>
193
+ <HookScene />
194
+ <Audio src={staticFile("voices/seg_01_hook.mp3")} />
195
+ </TransitionSeries.Sequence>
196
+
197
+ <TransitionSeries.Transition ... />
198
+
199
+ <TransitionSeries.Sequence durationInFrames={SCENES.intro.duration}>
200
+ <IntroScene />
201
+ <Audio src={staticFile("voices/seg_02_intro.mp3")} />
202
+ </TransitionSeries.Sequence>
203
+
204
+ {/* ... 更多场景 */}
205
+ </TransitionSeries>
206
+ );
207
+ };
208
+
209
+ // Root.tsx
210
+ <Composition
211
+ id="MyVideo"
212
+ component={MainVideo}
213
+ durationInFrames={TOTAL_DURATION}
214
+ fps={30}
215
+ width={1920}
216
+ height={1080}
217
+ />
218
+ ```
219
+
220
+ ## 时间线验证输出
221
+
222
+ 生成时输出详细信息:
223
+ ```
224
+ ✅ Timeline config generated: 9 scenes, 3570 frames (119.0s)
225
+
226
+ Scene timings:
227
+ hook : 8.86s audio → 295 frames (start: 0)
228
+ intro : 12.66s audio → 401 frames (start: 275)
229
+ what : 14.64s audio → 459 frames (start: 656)
230
+ demo : 15.33s audio → 480 frames (start: 1095)
231
+ tools : 15.33s audio → 480 frames (start: 1555)
232
+ features : 14.53s audio → 451 frames (start: 2015)
233
+ subagent : 12.81s audio → 398 frames (start: 2446)
234
+ install : 10.62s audio → 338 frames (start: 2824)
235
+ cta : 10.14s audio → 318 frames (start: 3142)
236
+
237
+ Total duration: 3570 frames = 119.0 seconds
238
+ ```
239
+
240
+ ## 优势
241
+
242
+ ✅ **消除手动错误**: 不再需要手动填写帧数
243
+ ✅ **自动同步**: 重新生成配音后自动更新时间线
244
+ ✅ **精确对齐**: 基于 VTT 毫秒级时间戳
245
+ ✅ **类型安全**: 生成 TypeScript 配置,编译时检查
246
+ ✅ **便于调试**: 可直接查看生成的配置文件
247
+
248
+ ## 注意事项
249
+
250
+ - 确保 VTT 文件命名规范(如 `seg_01_hook.vtt`)
251
+ - 缓冲时间(BUFFER)可根据视频节奏调整(0.3-1.0秒)
252
+ - 转场重叠(TRANSITION)通常为 10-15 帧(0.33-0.5秒)
253
+ - 生成的配置文件应添加到 `.gitignore`(每次构建重新生成)