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,353 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video-preflight-check
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: 视频预检技能。在渲染前检查音视频同步、字幕覆盖、时长合理性等质量问题。
|
|
5
|
+
triggers:
|
|
6
|
+
- 视频检查
|
|
7
|
+
- 预检
|
|
8
|
+
- 质量检查
|
|
9
|
+
- preflight
|
|
10
|
+
tools:
|
|
11
|
+
- Read
|
|
12
|
+
- Bash
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# 视频预检技能 (Video Preflight Check)
|
|
16
|
+
|
|
17
|
+
在视频渲染前进行全面质量检查,提前发现并修复问题,避免渲染后才发现错误。
|
|
18
|
+
|
|
19
|
+
## 检查项目清单
|
|
20
|
+
|
|
21
|
+
| 检查项 | 说明 | 严重程度 |
|
|
22
|
+
|--------|------|---------|
|
|
23
|
+
| 音视频同步 | 场景时长是否匹配音频时长 | 🔴 严重 |
|
|
24
|
+
| 字幕覆盖 | 是否有无字幕的语音片段 | 🟡 警告 |
|
|
25
|
+
| 长静默检测 | 是否有超过2秒的无内容等待 | 🟡 警告 |
|
|
26
|
+
| 文件完整性 | 所有引用的资源文件是否存在 | 🔴 严重 |
|
|
27
|
+
| 时长合理性 | 总时长是否在预期范围内 | 🟢 提示 |
|
|
28
|
+
| 过渡平滑性 | 场景间是否有过渡重叠 | 🟢 提示 |
|
|
29
|
+
|
|
30
|
+
## 检查脚本
|
|
31
|
+
|
|
32
|
+
### 1. 音视频同步检查
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// scripts/preflight-check.ts
|
|
36
|
+
import * as fs from 'fs';
|
|
37
|
+
import * as path from 'path';
|
|
38
|
+
|
|
39
|
+
interface CheckResult {
|
|
40
|
+
passed: boolean;
|
|
41
|
+
severity: 'error' | 'warning' | 'info';
|
|
42
|
+
message: string;
|
|
43
|
+
details?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const FPS = 30;
|
|
47
|
+
const BUFFER_TOLERANCE = 1.5; // 允许的缓冲误差(秒)
|
|
48
|
+
|
|
49
|
+
// 解析VTT获取音频时长
|
|
50
|
+
function getAudioDuration(vttPath: string): number {
|
|
51
|
+
const content = fs.readFileSync(vttPath, 'utf-8');
|
|
52
|
+
let maxEndTime = 0;
|
|
53
|
+
|
|
54
|
+
const matches = content.matchAll(/(\d{2}):(\d{2}):(\d{2})[,\.](\d{3})\s*-->\s*(\d{2}):(\d{2}):(\d{2})[,\.](\d{3})/g);
|
|
55
|
+
|
|
56
|
+
for (const match of matches) {
|
|
57
|
+
const endTime =
|
|
58
|
+
parseInt(match[5]) * 3600 +
|
|
59
|
+
parseInt(match[6]) * 60 +
|
|
60
|
+
parseInt(match[7]) +
|
|
61
|
+
parseInt(match[8]) / 1000;
|
|
62
|
+
maxEndTime = Math.max(maxEndTime, endTime);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return maxEndTime;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 检查音视频同步
|
|
69
|
+
function checkAudioVideoSync(scenes: Record<string, { duration: number }>, vttDir: string): CheckResult[] {
|
|
70
|
+
const results: CheckResult[] = [];
|
|
71
|
+
|
|
72
|
+
const vttFiles = fs.readdirSync(vttDir).filter(f => f.endsWith('.vtt'));
|
|
73
|
+
|
|
74
|
+
for (const vttFile of vttFiles) {
|
|
75
|
+
const sceneName = vttFile.replace(/^seg_\d+_/, '').replace('.vtt', '');
|
|
76
|
+
const scene = scenes[sceneName];
|
|
77
|
+
|
|
78
|
+
if (!scene) {
|
|
79
|
+
results.push({
|
|
80
|
+
passed: false,
|
|
81
|
+
severity: 'warning',
|
|
82
|
+
message: `未找到场景配置: ${sceneName}`,
|
|
83
|
+
});
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const audioDuration = getAudioDuration(path.join(vttDir, vttFile));
|
|
88
|
+
const sceneDuration = scene.duration / FPS;
|
|
89
|
+
const diff = sceneDuration - audioDuration;
|
|
90
|
+
|
|
91
|
+
if (diff < 0) {
|
|
92
|
+
// 场景时长小于音频时长 - 音频会被截断
|
|
93
|
+
results.push({
|
|
94
|
+
passed: false,
|
|
95
|
+
severity: 'error',
|
|
96
|
+
message: `🔴 [${sceneName}] 音频被截断!`,
|
|
97
|
+
details: `场景时长: ${sceneDuration.toFixed(2)}s, 音频时长: ${audioDuration.toFixed(2)}s, 差值: ${diff.toFixed(2)}s`,
|
|
98
|
+
});
|
|
99
|
+
} else if (diff > BUFFER_TOLERANCE) {
|
|
100
|
+
// 场景时长比音频长太多 - 会有等待
|
|
101
|
+
results.push({
|
|
102
|
+
passed: false,
|
|
103
|
+
severity: 'warning',
|
|
104
|
+
message: `🟡 [${sceneName}] 等待时间过长`,
|
|
105
|
+
details: `场景时长: ${sceneDuration.toFixed(2)}s, 音频时长: ${audioDuration.toFixed(2)}s, 等待: ${diff.toFixed(2)}s`,
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
results.push({
|
|
109
|
+
passed: true,
|
|
110
|
+
severity: 'info',
|
|
111
|
+
message: `✅ [${sceneName}] 同步正常`,
|
|
112
|
+
details: `场景: ${sceneDuration.toFixed(2)}s, 音频: ${audioDuration.toFixed(2)}s`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return results;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 2. 字幕覆盖检查
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// 检查字幕是否覆盖所有语音
|
|
125
|
+
function checkSubtitleCoverage(
|
|
126
|
+
subtitles: Record<string, Array<{ start: number; end: number; text: string }>>,
|
|
127
|
+
scenes: Record<string, { start: number; duration: number }>
|
|
128
|
+
): CheckResult[] {
|
|
129
|
+
const results: CheckResult[] = [];
|
|
130
|
+
|
|
131
|
+
for (const [sceneName, cues] of Object.entries(subtitles)) {
|
|
132
|
+
const scene = scenes[sceneName];
|
|
133
|
+
if (!scene) continue;
|
|
134
|
+
|
|
135
|
+
// 计算字幕覆盖率
|
|
136
|
+
let coveredFrames = 0;
|
|
137
|
+
for (const cue of cues) {
|
|
138
|
+
coveredFrames += cue.end - cue.start;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const coverage = coveredFrames / scene.duration;
|
|
142
|
+
|
|
143
|
+
if (coverage < 0.5) {
|
|
144
|
+
results.push({
|
|
145
|
+
passed: false,
|
|
146
|
+
severity: 'warning',
|
|
147
|
+
message: `🟡 [${sceneName}] 字幕覆盖率低: ${(coverage * 100).toFixed(1)}%`,
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
results.push({
|
|
151
|
+
passed: true,
|
|
152
|
+
severity: 'info',
|
|
153
|
+
message: `✅ [${sceneName}] 字幕覆盖率: ${(coverage * 100).toFixed(1)}%`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 检查字幕间隙
|
|
158
|
+
const sortedCues = [...cues].sort((a, b) => a.start - b.start);
|
|
159
|
+
for (let i = 1; i < sortedCues.length; i++) {
|
|
160
|
+
const gap = sortedCues[i].start - sortedCues[i - 1].end;
|
|
161
|
+
if (gap > FPS * 2) { // 超过2秒间隙
|
|
162
|
+
results.push({
|
|
163
|
+
passed: false,
|
|
164
|
+
severity: 'warning',
|
|
165
|
+
message: `🟡 [${sceneName}] 字幕间隙过大: ${(gap / FPS).toFixed(1)}s`,
|
|
166
|
+
details: `在 "${sortedCues[i - 1].text}" 和 "${sortedCues[i].text}" 之间`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return results;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 3. 文件完整性检查
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// 检查所有引用的文件是否存在
|
|
180
|
+
function checkFileIntegrity(projectDir: string): CheckResult[] {
|
|
181
|
+
const results: CheckResult[] = [];
|
|
182
|
+
|
|
183
|
+
const requiredDirs = [
|
|
184
|
+
'public/voices',
|
|
185
|
+
'src/components',
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (const dir of requiredDirs) {
|
|
189
|
+
const fullPath = path.join(projectDir, dir);
|
|
190
|
+
if (!fs.existsSync(fullPath)) {
|
|
191
|
+
results.push({
|
|
192
|
+
passed: false,
|
|
193
|
+
severity: 'error',
|
|
194
|
+
message: `🔴 目录不存在: ${dir}`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 检查音频文件
|
|
200
|
+
const voicesDir = path.join(projectDir, 'public/voices');
|
|
201
|
+
if (fs.existsSync(voicesDir)) {
|
|
202
|
+
const mp3Files = fs.readdirSync(voicesDir).filter(f => f.endsWith('.mp3'));
|
|
203
|
+
const vttFiles = fs.readdirSync(voicesDir).filter(f => f.endsWith('.vtt'));
|
|
204
|
+
|
|
205
|
+
for (const mp3 of mp3Files) {
|
|
206
|
+
const vtt = mp3.replace('.mp3', '.vtt');
|
|
207
|
+
if (!vttFiles.includes(vtt)) {
|
|
208
|
+
results.push({
|
|
209
|
+
passed: false,
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
message: `🟡 缺少字幕文件: ${vtt}`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
results.push({
|
|
217
|
+
passed: true,
|
|
218
|
+
severity: 'info',
|
|
219
|
+
message: `✅ 音频文件: ${mp3Files.length} 个, 字幕文件: ${vttFiles.length} 个`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return results;
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 4. 运行完整检查
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
function runPreflightCheck(projectDir: string, scenes: any, subtitles: any): void {
|
|
231
|
+
console.log('\n🔍 视频预检开始...\n');
|
|
232
|
+
console.log('═'.repeat(60));
|
|
233
|
+
|
|
234
|
+
let hasErrors = false;
|
|
235
|
+
let hasWarnings = false;
|
|
236
|
+
|
|
237
|
+
// 1. 文件完整性
|
|
238
|
+
console.log('\n📁 文件完整性检查');
|
|
239
|
+
console.log('-'.repeat(40));
|
|
240
|
+
const fileResults = checkFileIntegrity(projectDir);
|
|
241
|
+
for (const r of fileResults) {
|
|
242
|
+
console.log(r.message);
|
|
243
|
+
if (r.details) console.log(` ${r.details}`);
|
|
244
|
+
if (r.severity === 'error') hasErrors = true;
|
|
245
|
+
if (r.severity === 'warning') hasWarnings = true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 2. 音视频同步
|
|
249
|
+
console.log('\n🎵 音视频同步检查');
|
|
250
|
+
console.log('-'.repeat(40));
|
|
251
|
+
const syncResults = checkAudioVideoSync(scenes, path.join(projectDir, 'public/voices'));
|
|
252
|
+
for (const r of syncResults) {
|
|
253
|
+
console.log(r.message);
|
|
254
|
+
if (r.details) console.log(` ${r.details}`);
|
|
255
|
+
if (r.severity === 'error') hasErrors = true;
|
|
256
|
+
if (r.severity === 'warning') hasWarnings = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 3. 字幕覆盖
|
|
260
|
+
console.log('\n📝 字幕覆盖检查');
|
|
261
|
+
console.log('-'.repeat(40));
|
|
262
|
+
const subResults = checkSubtitleCoverage(subtitles, scenes);
|
|
263
|
+
for (const r of subResults) {
|
|
264
|
+
console.log(r.message);
|
|
265
|
+
if (r.details) console.log(` ${r.details}`);
|
|
266
|
+
if (r.severity === 'warning') hasWarnings = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 结果汇总
|
|
270
|
+
console.log('\n' + '═'.repeat(60));
|
|
271
|
+
if (hasErrors) {
|
|
272
|
+
console.log('❌ 预检失败 - 存在严重问题,请修复后再渲染');
|
|
273
|
+
process.exit(1);
|
|
274
|
+
} else if (hasWarnings) {
|
|
275
|
+
console.log('⚠️ 预检通过(有警告) - 建议检查后再渲染');
|
|
276
|
+
} else {
|
|
277
|
+
console.log('✅ 预检通过 - 可以开始渲染');
|
|
278
|
+
}
|
|
279
|
+
console.log('═'.repeat(60) + '\n');
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## 使用方法
|
|
284
|
+
|
|
285
|
+
### 命令行运行
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
# 在项目目录下运行
|
|
289
|
+
npx ts-node scripts/preflight-check.ts
|
|
290
|
+
|
|
291
|
+
# 或添加到 package.json
|
|
292
|
+
{
|
|
293
|
+
"scripts": {
|
|
294
|
+
"preflight": "ts-node scripts/preflight-check.ts",
|
|
295
|
+
"render": "npm run preflight && remotion render src/index.ts MyVideo out/video.mp4"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### 输出示例
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
🔍 视频预检开始...
|
|
304
|
+
|
|
305
|
+
════════════════════════════════════════════════════════════
|
|
306
|
+
|
|
307
|
+
📁 文件完整性检查
|
|
308
|
+
----------------------------------------
|
|
309
|
+
✅ 音频文件: 9 个, 字幕文件: 9 个
|
|
310
|
+
|
|
311
|
+
🎵 音视频同步检查
|
|
312
|
+
----------------------------------------
|
|
313
|
+
✅ [hook] 同步正常
|
|
314
|
+
场景: 9.83s, 音频: 8.86s
|
|
315
|
+
✅ [intro] 同步正常
|
|
316
|
+
场景: 13.37s, 音频: 12.66s
|
|
317
|
+
🟡 [demo] 等待时间过长
|
|
318
|
+
场景: 25.00s, 音频: 15.33s, 等待: 9.67s
|
|
319
|
+
|
|
320
|
+
📝 字幕覆盖检查
|
|
321
|
+
----------------------------------------
|
|
322
|
+
✅ [hook] 字幕覆盖率: 85.2%
|
|
323
|
+
🟡 [demo] 字幕覆盖率: 45.3%
|
|
324
|
+
|
|
325
|
+
════════════════════════════════════════════════════════════
|
|
326
|
+
⚠️ 预检通过(有警告) - 建议检查后再渲染
|
|
327
|
+
════════════════════════════════════════════════════════════
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## 集成到工作流
|
|
331
|
+
|
|
332
|
+
在渲染前自动运行预检:
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// 在 crafter agent 中
|
|
336
|
+
async function renderVideo() {
|
|
337
|
+
// 1. 运行预检
|
|
338
|
+
const preflightResult = await runPreflightCheck(projectDir, SCENES, SUBTITLES);
|
|
339
|
+
|
|
340
|
+
if (preflightResult.hasErrors) {
|
|
341
|
+
throw new Error('预检失败,请先修复问题');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (preflightResult.hasWarnings) {
|
|
345
|
+
// 询问用户是否继续
|
|
346
|
+
const shouldContinue = await askUser('存在警告,是否继续渲染?');
|
|
347
|
+
if (!shouldContinue) return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 2. 开始渲染
|
|
351
|
+
await exec('npx remotion render ...');
|
|
352
|
+
}
|
|
353
|
+
```
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: voice-synthesizer
|
|
3
|
+
description: 语音合成技能。使用阿里云 cosyvoice-v3-flash 将文本转为配音音频,支持多语言、多音色、SSML标记。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## 概述
|
|
7
|
+
|
|
8
|
+
本技能使用阿里云百炼平台的 **cosyvoice-v3-flash** 模型进行语音合成。
|
|
9
|
+
|
|
10
|
+
**核心特性**:
|
|
11
|
+
- 高质量语音合成(最高 48kHz 采样率)
|
|
12
|
+
- 丰富的预置音色(70+ 种)
|
|
13
|
+
- 支持 SSML 标记语言(精细控制停顿、语速、重音)
|
|
14
|
+
- 支持多语言(中文、英文、粤语、日语、韩语等)
|
|
15
|
+
|
|
16
|
+
**限流**: 3 RPS(每秒请求数)
|
|
17
|
+
|
|
18
|
+
## 环境配置
|
|
19
|
+
|
|
20
|
+
### 1. 环境检查(先确认再操作)
|
|
21
|
+
|
|
22
|
+
如未安装 Python(`python`/`python3` 命令不存在),需要先安装 Python 3.9+,再继续后续步骤。
|
|
23
|
+
|
|
24
|
+
如系统仅提供 `python3`,将下方命令中的 `python` 替换为 `python3`。
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 检查 Python 版本(建议 3.9+)
|
|
28
|
+
python --version
|
|
29
|
+
|
|
30
|
+
# 检查 pip 是否可用
|
|
31
|
+
python -m pip --version
|
|
32
|
+
|
|
33
|
+
# 检查是否已有依赖(已安装则无需重复装)
|
|
34
|
+
python -m pip show dashscope python-dotenv
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
如未安装 `pip`,先执行:
|
|
38
|
+
```bash
|
|
39
|
+
python -m ensurepip --upgrade
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
如 `pip` 版本过低,先升级:
|
|
43
|
+
```bash
|
|
44
|
+
python -m pip install --upgrade pip
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. 安装依赖(仅在缺失时)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install dashscope python-dotenv
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 3. 配置 API Key(先检查是否已配置)
|
|
54
|
+
|
|
55
|
+
**方式 1: 环境变量**
|
|
56
|
+
```bash
|
|
57
|
+
export DASHSCOPE_API_KEY=your-api-key-here
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**方式 2: .env 配置文件(推荐)**
|
|
61
|
+
|
|
62
|
+
先检查 `.env` 是否已存在:
|
|
63
|
+
```bash
|
|
64
|
+
ls -a | grep -E '^\.env$'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
如果存在 `.env`,再检查是否已配置 `DASHSCOPE_API_KEY`(无输出表示未配置):
|
|
68
|
+
```bash
|
|
69
|
+
grep -E '^DASHSCOPE_API_KEY=' .env
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
如果 `.env` 不存在或未配置 `DASHSCOPE_API_KEY`,需要**手动补充配置**(不要跳过):
|
|
73
|
+
```env
|
|
74
|
+
DASHSCOPE_API_KEY=sk-xxxxxxxxxxxxxxxx
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 基础用法
|
|
78
|
+
|
|
79
|
+
### 单个文本合成
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import dashscope
|
|
83
|
+
from dashscope.audio.tts_v2 import SpeechSynthesizer
|
|
84
|
+
import os
|
|
85
|
+
|
|
86
|
+
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
|
|
87
|
+
|
|
88
|
+
# 每次调用需重新实例化
|
|
89
|
+
synthesizer = SpeechSynthesizer(model="cosyvoice-v3-flash", voice="longanyang")
|
|
90
|
+
audio = synthesizer.call("今天天气怎么样?")
|
|
91
|
+
|
|
92
|
+
with open('output.mp3', 'wb') as f:
|
|
93
|
+
f.write(audio)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 批量合成
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
segments = [
|
|
100
|
+
{"id": "seg_01", "text": "欢迎使用我们的产品"},
|
|
101
|
+
{"id": "seg_02", "text": "首先,让我们看看主界面"},
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
for seg in segments:
|
|
105
|
+
synthesizer = SpeechSynthesizer(model="cosyvoice-v3-flash", voice="longanyang")
|
|
106
|
+
audio = synthesizer.call(seg["text"])
|
|
107
|
+
with open(f"./voices/{seg['id']}.mp3", 'wb') as f:
|
|
108
|
+
f.write(audio)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 推荐音色
|
|
112
|
+
|
|
113
|
+
### 社交陪伴(标杆音色,支持Instruct)
|
|
114
|
+
|
|
115
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
116
|
+
|------|-----------|------|------|
|
|
117
|
+
| 龙安洋 | longanyang | 阳光大男孩 | 20~30岁 |
|
|
118
|
+
| 龙安欢 | longanhuan | 欢脱元气女 | 20~30岁 |
|
|
119
|
+
|
|
120
|
+
### 社交陪伴(通用)
|
|
121
|
+
|
|
122
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
123
|
+
|------|-----------|------|------|
|
|
124
|
+
| 龙橙 | longcheng_v3 | 智慧青年男 | 20~25岁 |
|
|
125
|
+
| 龙泽 | longze_v3 | 温暖元气男 | 25~30岁 |
|
|
126
|
+
| 龙哲 | longzhe_v3 | 呆板大暖男 | 25~30岁 |
|
|
127
|
+
| 龙颜 | longyan_v3 | 温暖春风女 | 30~35岁 |
|
|
128
|
+
| 龙星 | longxing_v3 | 温婉邻家女 | 20~25岁 |
|
|
129
|
+
| 龙天 | longtian_v3 | 磁性理智男 | 30~35岁 |
|
|
130
|
+
| 龙婉 | longwan_v3 | 细腻柔声女 | 20~30岁 |
|
|
131
|
+
| 龙嫱 | longqiang_v3 | 浪漫风情女 | 30~35岁 |
|
|
132
|
+
| 龙菲菲 | longfeifei_v3 | 甜美娇气女 | 20~25岁 |
|
|
133
|
+
| 龙浩 | longhao_v3 | 多情忧郁男 | 30~35岁 |
|
|
134
|
+
| 龙安柔 | longanrou_v3 | 温柔闺蜜女 | 20~35岁 |
|
|
135
|
+
| 龙寒 | longhan_v3 | 温暖痴情男 | 30~35岁 |
|
|
136
|
+
| 龙安智 | longanzhi_v3 | 睿智轻熟男 | 25~35岁 |
|
|
137
|
+
| 龙安灵 | longanling_v3 | 思维灵动女 | 20~30岁 |
|
|
138
|
+
| 龙安雅 | longanya_v3 | 高雅气质女 | 25~35岁 |
|
|
139
|
+
| 龙安亲 | longanqin_v3 | 亲和活泼女 | 20~25岁 |
|
|
140
|
+
|
|
141
|
+
### 语音助手
|
|
142
|
+
|
|
143
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
144
|
+
|------|-----------|------|------|
|
|
145
|
+
| 龙小淳 | longxiaochun_v3 | 知性积极女 | 25~30岁 |
|
|
146
|
+
| 龙小夏 | longxiaoxia_v3 | 沉稳权威女 | 25~30岁 |
|
|
147
|
+
| YUMI | longyumi_v3 | 正经青年女 | 20~25岁 |
|
|
148
|
+
| 龙安昀 | longanyun_v3 | 居家暖男 | 30~35岁 |
|
|
149
|
+
| 龙安温 | longanwen_v3 | 优雅知性女 | 25~35岁 |
|
|
150
|
+
| 龙安莉 | longanli_v3 | 利落从容女 | 25~35岁 |
|
|
151
|
+
| 龙安朗 | longanlang_v3 | 清爽利落男 | 20~25岁 |
|
|
152
|
+
| 龙应沐 | longyingmu_v3 | 优雅知性女 | 25~30岁 |
|
|
153
|
+
|
|
154
|
+
### 有声书
|
|
155
|
+
|
|
156
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
157
|
+
|------|-----------|------|------|
|
|
158
|
+
| 龙三叔 | longsanshu_v3 | 沉稳质感男 | 25~45岁 |
|
|
159
|
+
| 龙媛 | longyuan_v3 | 温暖治愈女 | 35~40岁 |
|
|
160
|
+
| 龙悦 | longyue_v3 | 温暖磁性女 | 30~35岁 |
|
|
161
|
+
| 龙修 | longxiu_v3 | 博才说书男 | 25~35岁 |
|
|
162
|
+
| 龙楠 | longnan_v3 | 睿智青年男 | 25~30岁 |
|
|
163
|
+
| 龙婉君 | longwanjun_v3 | 细腻柔声女 | 20~30岁 |
|
|
164
|
+
| 龙逸尘 | longyichen_v3 | 洒脱活力男 | 20~30岁 |
|
|
165
|
+
| 龙老伯 | longlaobo_v3 | 沧桑岁月爷 | 60岁以上 |
|
|
166
|
+
| 龙老姨 | longlaoyi_v3 | 烟火从容阿姨 | 60岁以上 |
|
|
167
|
+
|
|
168
|
+
### 新闻播报
|
|
169
|
+
|
|
170
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
171
|
+
|------|-----------|------|------|
|
|
172
|
+
| 龙书 | longshu_v3 | 沉稳青年男 | 20~25岁 |
|
|
173
|
+
| Bella3.0 | loongbella_v3 | 精准干练女 | 25~30岁 |
|
|
174
|
+
|
|
175
|
+
### 客服
|
|
176
|
+
|
|
177
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
178
|
+
|------|-----------|------|------|
|
|
179
|
+
| 龙应询 | longyingxun_v3 | 年轻青涩男 | 20~25岁 |
|
|
180
|
+
| 龙应静 | longyingjing_v3 | 低调冷静女(支持时间戳) | 25~35岁 |
|
|
181
|
+
| 龙应聆 | longyingling_v3 | 温和共情女 | 25~30岁 |
|
|
182
|
+
| 龙应桃 | longyingtao_v3 | 温柔淡定女 | 25~30岁 |
|
|
183
|
+
|
|
184
|
+
### 直播带货
|
|
185
|
+
|
|
186
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
187
|
+
|------|-----------|------|------|
|
|
188
|
+
| 龙应笑 | longyingxiao_v3 | 清甜推销女 | 20~25岁 |
|
|
189
|
+
| 龙安宣 | longanxuan_v3 | 经典直播女 | 30~40岁 |
|
|
190
|
+
|
|
191
|
+
### 童声
|
|
192
|
+
|
|
193
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
194
|
+
|------|-----------|------|------|
|
|
195
|
+
| 龙呼呼 | longhuhu_v3 | 天真烂漫女童(支持Instruct) | 6~10岁 |
|
|
196
|
+
|
|
197
|
+
### 短视频配音
|
|
198
|
+
|
|
199
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
200
|
+
|------|-----------|------|------|
|
|
201
|
+
| 龙机器 | longjiqi_v3 | 呆萌机器人 | 20~30岁 |
|
|
202
|
+
| 龙猴哥 | longhouge_v3 | 经典猴哥 | 20~25岁 |
|
|
203
|
+
| 龙黛玉 | longdaiyu_v3 | 娇率才女音 | 15~25岁 |
|
|
204
|
+
|
|
205
|
+
### 诗词朗诵
|
|
206
|
+
|
|
207
|
+
| 名称 | voice参数 | 特点 | 年龄 |
|
|
208
|
+
|------|-----------|------|------|
|
|
209
|
+
| 龙飞 | longfei_v3 | 热血磁性男 | 30~35岁 |
|
|
210
|
+
|
|
211
|
+
### 方言
|
|
212
|
+
|
|
213
|
+
| 名称 | voice参数 | 特点 | 语言 |
|
|
214
|
+
|------|-----------|------|------|
|
|
215
|
+
| 龙嘉怡 | longjiayi_v3 | 知性粤语女 | 粤语 |
|
|
216
|
+
| 龙安粤 | longanyue_v3 | 欢脱粤语男 | 粤语 |
|
|
217
|
+
| 龙老铁 | longlaotie_v3 | 东北直率男 | 东北话 |
|
|
218
|
+
| 龙陕哥 | longshange_v3 | 原味陕北男 | 陕西话 |
|
|
219
|
+
| 龙安闽 | longanmin_v3 | 清纯萝莉女 | 闽南话 |
|
|
220
|
+
|
|
221
|
+
### 出海营销
|
|
222
|
+
|
|
223
|
+
| 名称 | voice参数 | 特点 | 语言 |
|
|
224
|
+
|------|-----------|------|------|
|
|
225
|
+
| loongkyong | loongkyong_v3 | 韩语女 | 韩语 |
|
|
226
|
+
| loongtomoka | loongtomoka_v3 | 日语女 | 日语 |
|
|
227
|
+
|
|
228
|
+
## 高级参数
|
|
229
|
+
|
|
230
|
+
### 音频格式
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from dashscope.audio.tts_v2 import AudioFormat
|
|
234
|
+
|
|
235
|
+
synthesizer = SpeechSynthesizer(
|
|
236
|
+
model="cosyvoice-v3-flash",
|
|
237
|
+
voice="longanyang",
|
|
238
|
+
format=AudioFormat.MP3_22050HZ_MONO_256KBPS # 默认
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
可选格式:
|
|
243
|
+
- `MP3_22050HZ_MONO_256KBPS` (默认)
|
|
244
|
+
- `MP3_48000HZ_MONO_256KBPS` (高音质)
|
|
245
|
+
- `WAV_22050HZ_MONO_16BIT`
|
|
246
|
+
- `PCM_22050HZ_MONO_16BIT`
|
|
247
|
+
|
|
248
|
+
### 语速、音调、音量
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
synthesizer = SpeechSynthesizer(
|
|
252
|
+
model="cosyvoice-v3-flash",
|
|
253
|
+
voice="longanyang",
|
|
254
|
+
speech_rate=1.1, # 语速 [0.5, 2.0],默认 1.0
|
|
255
|
+
pitch_rate=1.05, # 音调 [0.5, 2.0],默认 1.0
|
|
256
|
+
volume=60 # 音量 [0, 100],默认 50
|
|
257
|
+
)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## CLI 工具使用
|
|
261
|
+
|
|
262
|
+
### 单个文本合成
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
python .claude/skills/voice-synthesizer/scripts/tts_cli.py \
|
|
266
|
+
--text "今天天气怎么样" \
|
|
267
|
+
--voice longanyang \
|
|
268
|
+
--output ./voices/output.mp3
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 批量合成
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
python .claude/skills/voice-synthesizer/scripts/tts_cli.py \
|
|
275
|
+
--batch segments.json \
|
|
276
|
+
--output-dir ./voices/
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`segments.json` 格式:
|
|
280
|
+
```json
|
|
281
|
+
[
|
|
282
|
+
{"id": "seg_01", "text": "欢迎使用我们的产品"},
|
|
283
|
+
{"id": "seg_02", "text": "首先,让我们看看主界面"}
|
|
284
|
+
]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## 注意事项
|
|
288
|
+
|
|
289
|
+
1. **API Key 安全**: 使用环境变量存储,不提交到代码仓库
|
|
290
|
+
2. **限流控制**: 3 RPS,批量合成时需要控制并发
|
|
291
|
+
3. **长文本处理**: 建议分段合成,每段不超过 500 字
|
|
292
|
+
4. **文本长度限制**: 单次最多 20,000 字符
|
|
293
|
+
|
|
294
|
+
## 相关文档
|
|
295
|
+
|
|
296
|
+
- [阿里云 CosyVoice 官方文档](https://help.aliyun.com/zh/model-studio/developer-reference/cosyvoice-large-speech-synthesis)
|