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,376 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: asset-validator
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: 素材验证技能。在视频渲染前验证所有素材的完整性和有效性,避免渲染到一半才发现问题。
|
|
5
|
+
triggers:
|
|
6
|
+
- 素材验证
|
|
7
|
+
- asset validation
|
|
8
|
+
- 预检查
|
|
9
|
+
- preflight
|
|
10
|
+
tools:
|
|
11
|
+
- Read
|
|
12
|
+
- Bash
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# 素材验证技能 (Asset Validator)
|
|
16
|
+
|
|
17
|
+
在 Remotion 渲染前自动验证所有素材文件是否存在、格式是否正确、时长是否合理,提前发现问题。
|
|
18
|
+
|
|
19
|
+
## 核心原理
|
|
20
|
+
|
|
21
|
+
**问题**: 渲染到一半才发现文件缺失或格式错误,浪费时间。
|
|
22
|
+
|
|
23
|
+
**解决**: 在渲染前执行完整性检查,问题清单化输出。
|
|
24
|
+
|
|
25
|
+
## 验证检查项
|
|
26
|
+
|
|
27
|
+
### 1. 文件存在性检查
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
interface FileCheck {
|
|
31
|
+
path: string;
|
|
32
|
+
exists: boolean;
|
|
33
|
+
size?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function checkFileExists(filePaths: string[]): FileCheck[] {
|
|
37
|
+
return filePaths.map(path => ({
|
|
38
|
+
path,
|
|
39
|
+
exists: fs.existsSync(path),
|
|
40
|
+
size: fs.existsSync(path) ? fs.statSync(path).size : undefined,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. MP3/VTT 配对检查
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
function checkAudioSubtitlePairs(voicesDir: string): {
|
|
49
|
+
missing_vtt: string[];
|
|
50
|
+
missing_mp3: string[];
|
|
51
|
+
orphaned_files: string[];
|
|
52
|
+
} {
|
|
53
|
+
const mp3Files = fs.readdirSync(voicesDir).filter(f => f.endsWith('.mp3'));
|
|
54
|
+
const vttFiles = fs.readdirSync(voicesDir).filter(f => f.endsWith('.vtt'));
|
|
55
|
+
|
|
56
|
+
const missing_vtt: string[] = [];
|
|
57
|
+
const missing_mp3: string[] = [];
|
|
58
|
+
|
|
59
|
+
for (const mp3 of mp3Files) {
|
|
60
|
+
const vtt = mp3.replace('.mp3', '.vtt');
|
|
61
|
+
if (!vttFiles.includes(vtt)) {
|
|
62
|
+
missing_vtt.push(vtt);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const vtt of vttFiles) {
|
|
67
|
+
const mp3 = vtt.replace('.vtt', '.mp3');
|
|
68
|
+
if (!mp3Files.includes(mp3)) {
|
|
69
|
+
missing_mp3.push(mp3);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { missing_vtt, missing_mp3, orphaned_files: [] };
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. 音频时长验证
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
async function getAudioDurationFFProbe(filePath: string): Promise<number> {
|
|
81
|
+
const { stdout } = await exec(
|
|
82
|
+
`ffprobe -v error -show_entries format=duration -of csv=p=0 "${filePath}"`
|
|
83
|
+
);
|
|
84
|
+
return parseFloat(stdout.trim());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function validateAudioDurations(audioFiles: string[]): Promise<{
|
|
88
|
+
valid: string[];
|
|
89
|
+
invalid: Array<{ file: string; duration: number; reason: string }>;
|
|
90
|
+
}> {
|
|
91
|
+
const valid: string[] = [];
|
|
92
|
+
const invalid: Array<{ file: string; duration: number; reason: string }> = [];
|
|
93
|
+
|
|
94
|
+
for (const file of audioFiles) {
|
|
95
|
+
const duration = await getAudioDurationFFProbe(file);
|
|
96
|
+
|
|
97
|
+
if (duration === 0) {
|
|
98
|
+
invalid.push({ file, duration, reason: '音频时长为0' });
|
|
99
|
+
} else if (duration < 1) {
|
|
100
|
+
invalid.push({ file, duration, reason: '音频时长过短 (<1s)' });
|
|
101
|
+
} else if (duration > 60) {
|
|
102
|
+
invalid.push({ file, duration, reason: '音频时长异常长 (>60s)' });
|
|
103
|
+
} else {
|
|
104
|
+
valid.push(file);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { valid, invalid };
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 4. VTT 格式验证
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
function validateVTTFormat(vttPath: string): {
|
|
116
|
+
valid: boolean;
|
|
117
|
+
errors: string[];
|
|
118
|
+
} {
|
|
119
|
+
const content = fs.readFileSync(vttPath, 'utf-8');
|
|
120
|
+
const errors: string[] = [];
|
|
121
|
+
|
|
122
|
+
// 检查 WEBVTT 头
|
|
123
|
+
if (!content.startsWith('WEBVTT')) {
|
|
124
|
+
errors.push('缺少 WEBVTT 文件头');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 检查时间戳格式
|
|
128
|
+
const timeRegex = /\d{2}:\d{2}:\d{2}[,\.]\d{3}\s*-->\s*\d{2}:\d{2}:\d{2}[,\.]\d{3}/g;
|
|
129
|
+
const timestamps = content.match(timeRegex);
|
|
130
|
+
|
|
131
|
+
if (!timestamps || timestamps.length === 0) {
|
|
132
|
+
errors.push('未找到有效的时间戳');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 检查是否有字幕文本
|
|
136
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
137
|
+
const textLines = lines.filter(l =>
|
|
138
|
+
!l.startsWith('WEBVTT') &&
|
|
139
|
+
!l.match(/^\d+$/) &&
|
|
140
|
+
!l.match(timeRegex)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
if (textLines.length === 0) {
|
|
144
|
+
errors.push('未找到字幕文本');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
valid: errors.length === 0,
|
|
149
|
+
errors,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 完整验证流程
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
interface ValidationResult {
|
|
158
|
+
passed: boolean;
|
|
159
|
+
errors: Array<{ severity: 'error' | 'warning'; message: string }>;
|
|
160
|
+
summary: {
|
|
161
|
+
total_files_checked: number;
|
|
162
|
+
files_missing: number;
|
|
163
|
+
audio_files: number;
|
|
164
|
+
vtt_files: number;
|
|
165
|
+
invalid_durations: number;
|
|
166
|
+
format_errors: number;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function validateAssets(projectDir: string): Promise<ValidationResult> {
|
|
171
|
+
const errors: Array<{ severity: 'error' | 'warning'; message: string }> = [];
|
|
172
|
+
const voicesDir = path.join(projectDir, 'public/voices');
|
|
173
|
+
|
|
174
|
+
console.log('\n🔍 Starting asset validation...\n');
|
|
175
|
+
|
|
176
|
+
// 1. 检查目录存在
|
|
177
|
+
if (!fs.existsSync(voicesDir)) {
|
|
178
|
+
errors.push({
|
|
179
|
+
severity: 'error',
|
|
180
|
+
message: `Voices directory not found: ${voicesDir}`,
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
passed: false,
|
|
184
|
+
errors,
|
|
185
|
+
summary: { total_files_checked: 0, files_missing: 0, audio_files: 0, vtt_files: 0, invalid_durations: 0, format_errors: 0 },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 2. 检查 MP3/VTT 配对
|
|
190
|
+
console.log('📁 Checking audio-subtitle pairs...');
|
|
191
|
+
const pairCheck = checkAudioSubtitlePairs(voicesDir);
|
|
192
|
+
|
|
193
|
+
if (pairCheck.missing_vtt.length > 0) {
|
|
194
|
+
pairCheck.missing_vtt.forEach(vtt => {
|
|
195
|
+
errors.push({ severity: 'error', message: `Missing VTT file: ${vtt}` });
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (pairCheck.missing_mp3.length > 0) {
|
|
200
|
+
pairCheck.missing_mp3.forEach(mp3 => {
|
|
201
|
+
errors.push({ severity: 'error', message: `Missing MP3 file: ${mp3}` });
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 3. 验证音频时长
|
|
206
|
+
console.log('🎵 Validating audio durations...');
|
|
207
|
+
const mp3Files = fs.readdirSync(voicesDir)
|
|
208
|
+
.filter(f => f.endsWith('.mp3'))
|
|
209
|
+
.map(f => path.join(voicesDir, f));
|
|
210
|
+
|
|
211
|
+
const durationCheck = await validateAudioDurations(mp3Files);
|
|
212
|
+
|
|
213
|
+
if (durationCheck.invalid.length > 0) {
|
|
214
|
+
durationCheck.invalid.forEach(({ file, duration, reason }) => {
|
|
215
|
+
errors.push({
|
|
216
|
+
severity: 'error',
|
|
217
|
+
message: `Invalid audio: ${path.basename(file)} - ${reason} (${duration.toFixed(2)}s)`,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// 4. 验证 VTT 格式
|
|
223
|
+
console.log('📝 Validating VTT format...');
|
|
224
|
+
const vttFiles = fs.readdirSync(voicesDir)
|
|
225
|
+
.filter(f => f.endsWith('.vtt'))
|
|
226
|
+
.map(f => path.join(voicesDir, f));
|
|
227
|
+
|
|
228
|
+
let formatErrors = 0;
|
|
229
|
+
for (const vttFile of vttFiles) {
|
|
230
|
+
const vttCheck = validateVTTFormat(vttFile);
|
|
231
|
+
if (!vttCheck.valid) {
|
|
232
|
+
formatErrors++;
|
|
233
|
+
vttCheck.errors.forEach(err => {
|
|
234
|
+
errors.push({
|
|
235
|
+
severity: 'error',
|
|
236
|
+
message: `VTT format error in ${path.basename(vttFile)}: ${err}`,
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 生成摘要
|
|
243
|
+
const summary = {
|
|
244
|
+
total_files_checked: mp3Files.length + vttFiles.length,
|
|
245
|
+
files_missing: pairCheck.missing_vtt.length + pairCheck.missing_mp3.length,
|
|
246
|
+
audio_files: mp3Files.length,
|
|
247
|
+
vtt_files: vttFiles.length,
|
|
248
|
+
invalid_durations: durationCheck.invalid.length,
|
|
249
|
+
format_errors: formatErrors,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const passed = errors.filter(e => e.severity === 'error').length === 0;
|
|
253
|
+
|
|
254
|
+
return { passed, errors, summary };
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## 使用方法
|
|
259
|
+
|
|
260
|
+
### 创建验证脚本
|
|
261
|
+
|
|
262
|
+
`scripts/validate-assets.ts`:
|
|
263
|
+
```typescript
|
|
264
|
+
import { validateAssets } from '../src/utils/asset-validator';
|
|
265
|
+
|
|
266
|
+
async function main() {
|
|
267
|
+
const projectDir = path.join(__dirname, '..');
|
|
268
|
+
const result = await validateAssets(projectDir);
|
|
269
|
+
|
|
270
|
+
console.log('\n' + '═'.repeat(60));
|
|
271
|
+
|
|
272
|
+
if (result.passed) {
|
|
273
|
+
console.log('✅ All assets validated successfully!');
|
|
274
|
+
console.log(`\n📊 Summary:`);
|
|
275
|
+
console.log(` Total files: ${result.summary.total_files_checked}`);
|
|
276
|
+
console.log(` Audio files: ${result.summary.audio_files}`);
|
|
277
|
+
console.log(` VTT files: ${result.summary.vtt_files}`);
|
|
278
|
+
} else {
|
|
279
|
+
console.log('❌ Asset validation failed!\n');
|
|
280
|
+
|
|
281
|
+
const errorCount = result.errors.filter(e => e.severity === 'error').length;
|
|
282
|
+
const warningCount = result.errors.filter(e => e.severity === 'warning').length;
|
|
283
|
+
|
|
284
|
+
console.log(`Errors: ${errorCount}, Warnings: ${warningCount}\n`);
|
|
285
|
+
|
|
286
|
+
result.errors.forEach(({ severity, message }) => {
|
|
287
|
+
const icon = severity === 'error' ? '🔴' : '🟡';
|
|
288
|
+
console.log(`${icon} ${message}`);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log('═'.repeat(60) + '\n');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
main();
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 集成到构建流程
|
|
301
|
+
|
|
302
|
+
`package.json`:
|
|
303
|
+
```json
|
|
304
|
+
{
|
|
305
|
+
"scripts": {
|
|
306
|
+
"validate:assets": "tsx scripts/validate-assets.ts",
|
|
307
|
+
"prebuild": "npm run validate:assets && npm run generate:timeline && npm run generate:subtitles",
|
|
308
|
+
"render": "npm run prebuild && remotion render ..."
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## 输出示例
|
|
314
|
+
|
|
315
|
+
### 成功情况
|
|
316
|
+
```
|
|
317
|
+
🔍 Starting asset validation...
|
|
318
|
+
|
|
319
|
+
📁 Checking audio-subtitle pairs...
|
|
320
|
+
✅ All audio files have matching VTT files
|
|
321
|
+
|
|
322
|
+
🎵 Validating audio durations...
|
|
323
|
+
✅ All audio files have valid durations
|
|
324
|
+
|
|
325
|
+
📝 Validating VTT format...
|
|
326
|
+
✅ All VTT files are properly formatted
|
|
327
|
+
|
|
328
|
+
═══════════════════════════════════════════════════════
|
|
329
|
+
✅ All assets validated successfully!
|
|
330
|
+
|
|
331
|
+
📊 Summary:
|
|
332
|
+
Total files: 18
|
|
333
|
+
Audio files: 9
|
|
334
|
+
VTT files: 9
|
|
335
|
+
═══════════════════════════════════════════════════════
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### 失败情况
|
|
339
|
+
```
|
|
340
|
+
🔍 Starting asset validation...
|
|
341
|
+
|
|
342
|
+
📁 Checking audio-subtitle pairs...
|
|
343
|
+
🔴 Missing VTT file: seg_05_tools.vtt
|
|
344
|
+
|
|
345
|
+
🎵 Validating audio durations...
|
|
346
|
+
🔴 Invalid audio: seg_02_intro.mp3 - 音频时长为0 (0.00s)
|
|
347
|
+
|
|
348
|
+
📝 Validating VTT format...
|
|
349
|
+
🔴 VTT format error in seg_03_what.vtt: 缺少 WEBVTT 文件头
|
|
350
|
+
|
|
351
|
+
═══════════════════════════════════════════════════════
|
|
352
|
+
❌ Asset validation failed!
|
|
353
|
+
|
|
354
|
+
Errors: 3, Warnings: 0
|
|
355
|
+
|
|
356
|
+
🔴 Missing VTT file: seg_05_tools.vtt
|
|
357
|
+
🔴 Invalid audio: seg_02_intro.mp3 - 音频时长为0 (0.00s)
|
|
358
|
+
🔴 VTT format error in seg_03_what.vtt: 缺少 WEBVTT 文件头
|
|
359
|
+
═══════════════════════════════════════════════════════
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## 优势
|
|
363
|
+
|
|
364
|
+
✅ **早期发现问题**: 渲染前而非渲染中发现问题
|
|
365
|
+
✅ **清晰的错误信息**: 明确指出哪个文件有什么问题
|
|
366
|
+
✅ **自动化**: 集成到构建流程,无需手动检查
|
|
367
|
+
✅ **全面检查**: 文件存在性、配对关系、格式正确性、时长合理性
|
|
368
|
+
|
|
369
|
+
## 扩展检查项
|
|
370
|
+
|
|
371
|
+
可根据需要添加更多检查:
|
|
372
|
+
- BGM 文件存在性和格式
|
|
373
|
+
- 录屏素材完整性
|
|
374
|
+
- 图片资源分辨率检查
|
|
375
|
+
- 字体文件可用性
|
|
376
|
+
- 组件代码语法检查
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audio-video-sync
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: 音视频同步技能。解析音频时长,自动计算场景时间线,确保画面与语音完美同步。
|
|
5
|
+
triggers:
|
|
6
|
+
- 音视频同步
|
|
7
|
+
- 场景时长
|
|
8
|
+
- 时间线
|
|
9
|
+
- sync
|
|
10
|
+
tools:
|
|
11
|
+
- Read
|
|
12
|
+
- Write
|
|
13
|
+
- Bash
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# 音视频同步技能 (Audio-Video Sync)
|
|
17
|
+
|
|
18
|
+
确保视频场景时长与音频时长精确匹配,消除"说完话还在等待"的问题。
|
|
19
|
+
|
|
20
|
+
## 核心原则
|
|
21
|
+
|
|
22
|
+
1. **音频优先** - 场景时长由音频时长决定,而非硬编码
|
|
23
|
+
2. **合理缓冲** - 音频结束后保留0.5-1秒缓冲,避免突兀切换
|
|
24
|
+
3. **平滑过渡** - 场景间重叠0.3-0.5秒实现交叉淡化
|
|
25
|
+
4. **自动计算** - 从VTT文件自动提取时长,无需手动计算
|
|
26
|
+
|
|
27
|
+
## 从VTT获取音频时长
|
|
28
|
+
|
|
29
|
+
VTT文件由 `voice-synthesizer` 技能生成,包含精确的时间戳。
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// 解析VTT获取音频结束时间(秒)
|
|
33
|
+
function getAudioDuration(vttPath: string): number {
|
|
34
|
+
const content = fs.readFileSync(vttPath, 'utf-8');
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
|
|
37
|
+
let maxEndTime = 0;
|
|
38
|
+
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
// 匹配时间戳行: 00:00:05,187 --> 00:00:08,862
|
|
41
|
+
const match = line.match(/\d{2}:\d{2}:\d{2}[,\.]\d{3}\s*-->\s*(\d{2}):(\d{2}):(\d{2})[,\.](\d{3})/);
|
|
42
|
+
if (match) {
|
|
43
|
+
const endTime =
|
|
44
|
+
parseInt(match[1]) * 3600 +
|
|
45
|
+
parseInt(match[2]) * 60 +
|
|
46
|
+
parseInt(match[3]) +
|
|
47
|
+
parseInt(match[4]) / 1000;
|
|
48
|
+
maxEndTime = Math.max(maxEndTime, endTime);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return maxEndTime;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## 场景时间线计算
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
interface SceneConfig {
|
|
60
|
+
name: string;
|
|
61
|
+
audioFile: string;
|
|
62
|
+
vttFile: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SceneTiming {
|
|
66
|
+
start: number; // 开始帧
|
|
67
|
+
duration: number; // 持续帧数
|
|
68
|
+
audioDuration: number; // 音频时长(秒)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const FPS = 30;
|
|
72
|
+
const BUFFER = 0.5; // 音频后缓冲(秒)
|
|
73
|
+
const TRANSITION = 10; // 过渡重叠(帧)
|
|
74
|
+
|
|
75
|
+
function calculateSceneTimings(scenes: SceneConfig[]): Record<string, SceneTiming> {
|
|
76
|
+
const timings: Record<string, SceneTiming> = {};
|
|
77
|
+
let currentFrame = 0;
|
|
78
|
+
|
|
79
|
+
scenes.forEach((scene, index) => {
|
|
80
|
+
// 从VTT获取实际音频时长
|
|
81
|
+
const audioDuration = getAudioDuration(scene.vttFile);
|
|
82
|
+
|
|
83
|
+
// 场景时长 = 音频时长 + 缓冲 + 过渡
|
|
84
|
+
const sceneDuration = Math.ceil((audioDuration + BUFFER) * FPS) + TRANSITION;
|
|
85
|
+
|
|
86
|
+
timings[scene.name] = {
|
|
87
|
+
start: currentFrame,
|
|
88
|
+
duration: sceneDuration,
|
|
89
|
+
audioDuration: audioDuration,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// 下一场景开始位置(考虑过渡重叠)
|
|
93
|
+
const isLast = index === scenes.length - 1;
|
|
94
|
+
currentFrame += Math.ceil((audioDuration + BUFFER) * FPS) - (isLast ? 0 : TRANSITION);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return timings;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## 生成 Remotion 配置
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// 场景配置
|
|
105
|
+
const SCENE_CONFIGS: SceneConfig[] = [
|
|
106
|
+
{ name: "hook", audioFile: "seg_01_hook.mp3", vttFile: "public/voices/seg_01_hook.vtt" },
|
|
107
|
+
{ name: "intro", audioFile: "seg_02_intro.mp3", vttFile: "public/voices/seg_02_intro.vtt" },
|
|
108
|
+
{ name: "what", audioFile: "seg_03_what.mp3", vttFile: "public/voices/seg_03_what.vtt" },
|
|
109
|
+
// ... 更多场景
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
// 计算时间线
|
|
113
|
+
const SCENES = calculateSceneTimings(SCENE_CONFIGS);
|
|
114
|
+
|
|
115
|
+
// 计算总时长
|
|
116
|
+
const TOTAL_DURATION = Object.values(SCENES).reduce(
|
|
117
|
+
(max, scene) => Math.max(max, scene.start + scene.duration),
|
|
118
|
+
0
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// 输出供 MainVideo.tsx 使用
|
|
122
|
+
console.log('Scene Timings:', SCENES);
|
|
123
|
+
console.log('Total Duration:', TOTAL_DURATION, 'frames', '=', TOTAL_DURATION / FPS, 'seconds');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 在 MainVideo.tsx 中使用
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import { AbsoluteFill, Audio, Sequence, staticFile } from "remotion";
|
|
130
|
+
|
|
131
|
+
// 由 audio-video-sync 计算生成
|
|
132
|
+
const SCENES = {
|
|
133
|
+
hook: { start: 0, duration: 295, audioDuration: 8.86 },
|
|
134
|
+
intro: { start: 275, duration: 401, audioDuration: 12.66 },
|
|
135
|
+
what: { start: 656, duration: 459, audioDuration: 14.64 },
|
|
136
|
+
// ...
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export const TOTAL_DURATION = 3570;
|
|
140
|
+
|
|
141
|
+
export const MainVideo: React.FC = () => {
|
|
142
|
+
return (
|
|
143
|
+
<AbsoluteFill>
|
|
144
|
+
<Sequence from={SCENES.hook.start} durationInFrames={SCENES.hook.duration}>
|
|
145
|
+
<HookScene />
|
|
146
|
+
<Audio src={staticFile("voices/seg_01_hook.mp3")} />
|
|
147
|
+
</Sequence>
|
|
148
|
+
|
|
149
|
+
<Sequence from={SCENES.intro.start} durationInFrames={SCENES.intro.duration}>
|
|
150
|
+
<IntroScene />
|
|
151
|
+
<Audio src={staticFile("voices/seg_02_intro.mp3")} />
|
|
152
|
+
</Sequence>
|
|
153
|
+
|
|
154
|
+
{/* ... 更多场景 */}
|
|
155
|
+
</AbsoluteFill>
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 时间线可视化
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
场景 | 音频时长 | 场景时长 | 开始帧 | 结束帧
|
|
164
|
+
----------|-----------|-----------|---------|--------
|
|
165
|
+
hook | 8.86s | 295帧 | 0 | 295
|
|
166
|
+
intro | 12.66s | 401帧 | 275 | 676
|
|
167
|
+
what | 14.64s | 459帧 | 656 | 1115
|
|
168
|
+
demo | 15.33s | 480帧 | 1095 | 1575
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
总时长: 119秒 = 3570帧 @ 30fps
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## 常见问题处理
|
|
175
|
+
|
|
176
|
+
### 问题1: 场景等待太久
|
|
177
|
+
**原因**: 场景时长 > 音频时长
|
|
178
|
+
**解决**: 使用此技能自动计算,确保 场景时长 ≈ 音频时长 + 0.5s
|
|
179
|
+
|
|
180
|
+
### 问题2: 音频被截断
|
|
181
|
+
**原因**: 场景时长 < 音频时长
|
|
182
|
+
**解决**: 检查VTT解析是否正确,确保读取到最后一条字幕的结束时间
|
|
183
|
+
|
|
184
|
+
### 问题3: 过渡突兀
|
|
185
|
+
**原因**: 没有场景重叠
|
|
186
|
+
**解决**: 设置 TRANSITION = 10-15 帧(0.3-0.5秒)实现交叉淡化
|
|
187
|
+
|
|
188
|
+
## 使用流程
|
|
189
|
+
|
|
190
|
+
1. **确保VTT文件存在**
|
|
191
|
+
```bash
|
|
192
|
+
ls public/voices/*.vtt
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
2. **运行时间线计算脚本**
|
|
196
|
+
```bash
|
|
197
|
+
npx ts-node scripts/calculate-timings.ts
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
3. **将输出复制到 MainVideo.tsx**
|
|
201
|
+
|
|
202
|
+
4. **更新 Root.tsx 中的 durationInFrames**
|
|
203
|
+
```tsx
|
|
204
|
+
<Composition
|
|
205
|
+
id="MyVideo"
|
|
206
|
+
component={MainVideo}
|
|
207
|
+
durationInFrames={TOTAL_DURATION}
|
|
208
|
+
fps={30}
|
|
209
|
+
width={1920}
|
|
210
|
+
height={1080}
|
|
211
|
+
/>
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## 质量检查
|
|
215
|
+
|
|
216
|
+
- [ ] 每个场景的 duration >= audioDuration * FPS
|
|
217
|
+
- [ ] 场景间过渡平滑(有重叠)
|
|
218
|
+
- [ ] 总时长合理(无多余空白)
|
|
219
|
+
- [ ] 音频不被截断
|