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.
- package/README.md +160 -0
- package/apps/web/README.md +36 -0
- package/apps/web/env.example +20 -0
- package/apps/web/eslint.config.mjs +18 -0
- package/apps/web/next.config.ts +7 -0
- package/apps/web/package-lock.json +10348 -0
- package/apps/web/package.json +35 -0
- package/apps/web/postcss.config.mjs +7 -0
- package/apps/web/public/file.svg +1 -0
- package/apps/web/public/globe.svg +1 -0
- package/apps/web/public/next.svg +1 -0
- package/apps/web/public/vercel.svg +1 -0
- package/apps/web/public/window.svg +1 -0
- package/apps/web/src/app/.well-known/agent-card.json/route.ts +50 -0
- package/apps/web/src/app/background-tasks/[jobId]/cancel/route.ts +29 -0
- package/apps/web/src/app/events/stream/route.ts +58 -0
- package/apps/web/src/app/favicon.ico +0 -0
- package/apps/web/src/app/globals.css +174 -0
- package/apps/web/src/app/layout.tsx +34 -0
- package/apps/web/src/app/messages/answer/route.ts +57 -0
- package/apps/web/src/app/messages/stream/route.ts +381 -0
- package/apps/web/src/app/page.tsx +358 -0
- package/apps/web/src/app/tasks/[taskId]/cancel/route.ts +24 -0
- package/apps/web/src/app/tasks/[taskId]/route.ts +24 -0
- package/apps/web/src/app/tasks/route.ts +13 -0
- package/apps/web/src/components/chat/agent-blocks.tsx +111 -0
- package/apps/web/src/components/chat/ask-user-question-panel.tsx +172 -0
- package/apps/web/src/components/chat/session-sidebar.tsx +222 -0
- package/apps/web/src/components/chat/subagent-activity-sidebar.tsx +248 -0
- package/apps/web/src/components/chat/tool-blocks.tsx +550 -0
- package/apps/web/src/lib/a2a/activity-store.ts +150 -0
- package/apps/web/src/lib/a2a/client.ts +357 -0
- package/apps/web/src/lib/a2a/sse.ts +19 -0
- package/apps/web/src/lib/a2a/task-store.ts +111 -0
- package/apps/web/src/lib/a2a/types.ts +216 -0
- package/apps/web/src/lib/agent/answer-store.ts +109 -0
- package/apps/web/src/lib/agent/background-delivery.ts +343 -0
- package/apps/web/src/lib/agent/background-tool.ts +78 -0
- package/apps/web/src/lib/agent/background.ts +452 -0
- package/apps/web/src/lib/agent/chat.ts +543 -0
- package/apps/web/src/lib/agent/session-store.ts +26 -0
- package/apps/web/src/lib/chat/types.ts +44 -0
- package/apps/web/src/lib/env.ts +31 -0
- package/apps/web/src/lib/hooks/useA2AChat.ts +863 -0
- package/apps/web/src/lib/state/chat-atoms.ts +52 -0
- package/apps/web/src/lib/workspace.ts +9 -0
- package/apps/web/tsconfig.json +35 -0
- package/bin/remotion-agent.js +451 -0
- package/package.json +34 -0
- package/templates/.claude/CLAUDE.md +95 -0
- package/templates/.claude/README.md +129 -0
- package/templates/.claude/agents/composer-agent.md +188 -0
- package/templates/.claude/agents/crafter.md +181 -0
- package/templates/.claude/agents/creator.md +134 -0
- package/templates/.claude/agents/perceiver.md +92 -0
- package/templates/.claude/settings.json +36 -0
- package/templates/.claude/settings.local.json +39 -0
- package/templates/.claude/skills/agent-browser/SKILL.md +349 -0
- package/templates/.claude/skills/agent-browser/references/authentication.md +188 -0
- package/templates/.claude/skills/agent-browser/references/proxy-support.md +175 -0
- package/templates/.claude/skills/agent-browser/references/session-management.md +181 -0
- package/templates/.claude/skills/agent-browser/references/snapshot-refs.md +186 -0
- package/templates/.claude/skills/agent-browser/references/video-recording.md +162 -0
- package/templates/.claude/skills/agent-browser/templates/authenticated-session.sh +91 -0
- package/templates/.claude/skills/agent-browser/templates/capture-workflow.sh +68 -0
- package/templates/.claude/skills/agent-browser/templates/form-automation.sh +64 -0
- package/templates/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
- package/templates/.claude/skills/algorithmic-art/SKILL.md +405 -0
- package/templates/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/templates/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
- package/templates/.claude/skills/asset-validator/SKILL.md +376 -0
- package/templates/.claude/skills/audio-video-sync/SKILL.md +219 -0
- package/templates/.claude/skills/bgm-manager/SKILL.md +334 -0
- package/templates/.claude/skills/remotion-best-practices/SKILL.md +45 -0
- package/templates/.claude/skills/remotion-best-practices/rules/3d.md +86 -0
- package/templates/.claude/skills/remotion-best-practices/rules/animations.md +29 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/templates/.claude/skills/remotion-best-practices/rules/assets.md +78 -0
- package/templates/.claude/skills/remotion-best-practices/rules/audio.md +172 -0
- package/templates/.claude/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
- package/templates/.claude/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/templates/.claude/skills/remotion-best-practices/rules/charts.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/compositions.md +141 -0
- package/templates/.claude/skills/remotion-best-practices/rules/display-captions.md +126 -0
- package/templates/.claude/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/templates/.claude/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
- package/templates/.claude/skills/remotion-best-practices/rules/gifs.md +138 -0
- package/templates/.claude/skills/remotion-best-practices/rules/images.md +130 -0
- package/templates/.claude/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
- package/templates/.claude/skills/remotion-best-practices/rules/lottie.md +68 -0
- package/templates/.claude/skills/remotion-best-practices/rules/maps.md +403 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
- package/templates/.claude/skills/remotion-best-practices/rules/measuring-text.md +143 -0
- package/templates/.claude/skills/remotion-best-practices/rules/parameters.md +98 -0
- package/templates/.claude/skills/remotion-best-practices/rules/sequencing.md +118 -0
- package/templates/.claude/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/templates/.claude/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/templates/.claude/skills/remotion-best-practices/rules/timing.md +179 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
- package/templates/.claude/skills/remotion-best-practices/rules/transitions.md +122 -0
- package/templates/.claude/skills/remotion-best-practices/rules/trimming.md +53 -0
- package/templates/.claude/skills/remotion-best-practices/rules/videos.md +171 -0
- package/templates/.claude/skills/remotion-components/SKILL.md +453 -0
- package/templates/.claude/skills/render-config/SKILL.md +290 -0
- package/templates/.claude/skills/script-writer/SKILL.md +59 -0
- package/templates/.claude/skills/style-director/script-writer/SKILL.md +82 -0
- package/templates/.claude/skills/style-director/style-director/SKILL.md +287 -0
- package/templates/.claude/skills/style-director/style-director/references/audience-and-scenarios.md +43 -0
- package/templates/.claude/skills/style-director/style-director/references/interaction-innovation.md +26 -0
- package/templates/.claude/skills/style-director/style-director/references/motion-grammar.md +66 -0
- package/templates/.claude/skills/style-director/style-director/references/quality-checklist.md +29 -0
- package/templates/.claude/skills/style-director/style-director/references/scene-recipes.md +38 -0
- package/templates/.claude/skills/style-director/style-director/references/visual-style-system.md +148 -0
- package/templates/.claude/skills/subtitle-composer/SKILL.md +304 -0
- package/templates/.claude/skills/subtitle-processor/SKILL.md +308 -0
- package/templates/.claude/skills/timeline-generator/SKILL.md +253 -0
- package/templates/.claude/skills/video-preflight-check/SKILL.md +353 -0
- package/templates/.claude/skills/voice-synthesizer/SKILL.md +296 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/synthesize_voice.py +315 -0
- package/templates/.claude/skills/voice-synthesizer/scripts/tts_cli.py +142 -0
- package/templates/.claude/skills/web-design-guidelines/SKILL.md +36 -0
- package/templates/.claude/skills/youtube-downloader/SKILL.md +99 -0
- 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`(每次构建重新生成)
|