@vibeframe/cli 0.27.0 → 0.29.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/LICENSE +21 -0
- package/dist/agent/adapters/index.d.ts +1 -0
- package/dist/agent/adapters/index.d.ts.map +1 -1
- package/dist/agent/adapters/index.js +5 -0
- package/dist/agent/adapters/index.js.map +1 -1
- package/dist/agent/adapters/openrouter.d.ts +16 -0
- package/dist/agent/adapters/openrouter.d.ts.map +1 -0
- package/dist/agent/adapters/openrouter.js +100 -0
- package/dist/agent/adapters/openrouter.js.map +1 -0
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +3 -1
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/setup.js +5 -2
- package/dist/commands/setup.js.map +1 -1
- package/dist/config/schema.d.ts +2 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/index.js +0 -0
- package/package.json +16 -12
- package/.turbo/turbo-build.log +0 -4
- package/.turbo/turbo-lint.log +0 -21
- package/.turbo/turbo-test.log +0 -689
- package/src/agent/adapters/claude.ts +0 -143
- package/src/agent/adapters/gemini.ts +0 -159
- package/src/agent/adapters/index.ts +0 -61
- package/src/agent/adapters/ollama.ts +0 -231
- package/src/agent/adapters/openai.ts +0 -116
- package/src/agent/adapters/xai.ts +0 -119
- package/src/agent/index.ts +0 -251
- package/src/agent/memory/index.ts +0 -151
- package/src/agent/prompts/system.ts +0 -106
- package/src/agent/tools/ai-editing.ts +0 -845
- package/src/agent/tools/ai-generation.ts +0 -1073
- package/src/agent/tools/ai-pipeline.ts +0 -1055
- package/src/agent/tools/ai.ts +0 -21
- package/src/agent/tools/batch.ts +0 -429
- package/src/agent/tools/e2e.test.ts +0 -545
- package/src/agent/tools/export.ts +0 -184
- package/src/agent/tools/filesystem.ts +0 -237
- package/src/agent/tools/index.ts +0 -150
- package/src/agent/tools/integration.test.ts +0 -775
- package/src/agent/tools/media.ts +0 -697
- package/src/agent/tools/project.ts +0 -313
- package/src/agent/tools/timeline.ts +0 -951
- package/src/agent/types.ts +0 -68
- package/src/commands/agent.ts +0 -340
- package/src/commands/ai-analyze.ts +0 -429
- package/src/commands/ai-animated-caption.ts +0 -390
- package/src/commands/ai-audio.ts +0 -941
- package/src/commands/ai-broll.ts +0 -490
- package/src/commands/ai-edit-cli.ts +0 -658
- package/src/commands/ai-edit.ts +0 -1542
- package/src/commands/ai-fill-gaps.ts +0 -566
- package/src/commands/ai-helpers.ts +0 -65
- package/src/commands/ai-highlights.ts +0 -1303
- package/src/commands/ai-image.ts +0 -761
- package/src/commands/ai-motion.ts +0 -347
- package/src/commands/ai-narrate.ts +0 -451
- package/src/commands/ai-review.ts +0 -309
- package/src/commands/ai-script-pipeline-cli.ts +0 -1710
- package/src/commands/ai-script-pipeline.ts +0 -1365
- package/src/commands/ai-suggest-edit.ts +0 -264
- package/src/commands/ai-video-fx.ts +0 -445
- package/src/commands/ai-video.ts +0 -915
- package/src/commands/ai-viral.ts +0 -595
- package/src/commands/ai-visual-fx.ts +0 -601
- package/src/commands/ai.test.ts +0 -627
- package/src/commands/ai.ts +0 -307
- package/src/commands/analyze.ts +0 -282
- package/src/commands/audio.ts +0 -644
- package/src/commands/batch.test.ts +0 -279
- package/src/commands/batch.ts +0 -440
- package/src/commands/detect.ts +0 -329
- package/src/commands/doctor.ts +0 -237
- package/src/commands/edit-cmd.ts +0 -1014
- package/src/commands/export.ts +0 -918
- package/src/commands/generate.ts +0 -2146
- package/src/commands/media.ts +0 -177
- package/src/commands/output.ts +0 -142
- package/src/commands/pipeline.ts +0 -398
- package/src/commands/project.test.ts +0 -127
- package/src/commands/project.ts +0 -149
- package/src/commands/sanitize.ts +0 -60
- package/src/commands/schema.ts +0 -130
- package/src/commands/setup.ts +0 -509
- package/src/commands/timeline.test.ts +0 -499
- package/src/commands/timeline.ts +0 -529
- package/src/commands/validate.ts +0 -77
- package/src/config/config.test.ts +0 -197
- package/src/config/index.ts +0 -125
- package/src/config/schema.ts +0 -82
- package/src/engine/index.ts +0 -2
- package/src/engine/project.test.ts +0 -702
- package/src/engine/project.ts +0 -439
- package/src/index.ts +0 -146
- package/src/utils/api-key.test.ts +0 -41
- package/src/utils/api-key.ts +0 -247
- package/src/utils/audio.ts +0 -83
- package/src/utils/exec-safe.ts +0 -75
- package/src/utils/first-run.ts +0 -52
- package/src/utils/provider-resolver.ts +0 -56
- package/src/utils/remotion.ts +0 -951
- package/src/utils/subtitle.test.ts +0 -227
- package/src/utils/subtitle.ts +0 -169
- package/src/utils/tty.ts +0 -196
- package/tsconfig.json +0 -20
|
@@ -1,390 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module ai-animated-caption
|
|
3
|
-
* @description Animated caption pipeline — word-by-word TikTok/Reels-style captions.
|
|
4
|
-
*
|
|
5
|
-
* Pipeline: Video → FFmpeg audio extract → Whisper word-level transcribe
|
|
6
|
-
* → Word grouping → Style routing (ASS fast tier / Remotion tier) → Output MP4
|
|
7
|
-
*
|
|
8
|
-
* ## Commands: vibe pipeline animated-caption
|
|
9
|
-
* ## Dependencies: Whisper (OpenAI), FFmpeg, Remotion (optional)
|
|
10
|
-
* @see MODELS.md for AI model configuration
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { resolve, dirname, basename } from "node:path";
|
|
14
|
-
import { writeFile, mkdir, rm } from "node:fs/promises";
|
|
15
|
-
import { existsSync } from "node:fs";
|
|
16
|
-
import { tmpdir } from "node:os";
|
|
17
|
-
import { transcribeWithWords } from "./ai-edit.js";
|
|
18
|
-
import { getApiKey } from "../utils/api-key.js";
|
|
19
|
-
import { execSafe, ffprobeVideoSize, ffprobeDuration } from "../utils/exec-safe.js";
|
|
20
|
-
import {
|
|
21
|
-
generateAnimatedCaptionComponent,
|
|
22
|
-
renderWithEmbeddedVideo,
|
|
23
|
-
} from "../utils/remotion.js";
|
|
24
|
-
|
|
25
|
-
// ── Types ─────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export interface WordTiming {
|
|
28
|
-
word: string;
|
|
29
|
-
start: number;
|
|
30
|
-
end: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface WordGroup {
|
|
34
|
-
words: WordTiming[];
|
|
35
|
-
startTime: number;
|
|
36
|
-
endTime: number;
|
|
37
|
-
text: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type AnimatedCaptionStyle =
|
|
41
|
-
| "highlight"
|
|
42
|
-
| "bounce"
|
|
43
|
-
| "pop-in"
|
|
44
|
-
| "neon"
|
|
45
|
-
| "karaoke-sweep"
|
|
46
|
-
| "typewriter";
|
|
47
|
-
|
|
48
|
-
const ASS_STYLES: AnimatedCaptionStyle[] = ["karaoke-sweep", "typewriter"];
|
|
49
|
-
|
|
50
|
-
export interface AnimatedCaptionOptions {
|
|
51
|
-
videoPath: string;
|
|
52
|
-
outputPath: string;
|
|
53
|
-
style: AnimatedCaptionStyle;
|
|
54
|
-
highlightColor: string;
|
|
55
|
-
fontSize?: number;
|
|
56
|
-
position: "top" | "center" | "bottom";
|
|
57
|
-
wordsPerGroup?: number;
|
|
58
|
-
maxChars?: number;
|
|
59
|
-
language?: string;
|
|
60
|
-
fast?: boolean;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface AnimatedCaptionResult {
|
|
64
|
-
success: boolean;
|
|
65
|
-
outputPath?: string;
|
|
66
|
-
wordCount?: number;
|
|
67
|
-
groupCount?: number;
|
|
68
|
-
style?: string;
|
|
69
|
-
tier?: "ass" | "remotion";
|
|
70
|
-
error?: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ── Word Grouping ─────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
const SENTENCE_BREAKS = /[.!?]/;
|
|
76
|
-
const CLAUSE_BREAKS = /[,;:]/;
|
|
77
|
-
const LONG_PAUSE_THRESHOLD = 0.5; // seconds
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Group words into display groups for animated captions.
|
|
81
|
-
* Groups by natural sentence boundaries, pauses, and word/char limits.
|
|
82
|
-
*/
|
|
83
|
-
export function groupWords(
|
|
84
|
-
words: WordTiming[],
|
|
85
|
-
options: { wordsPerGroup?: number; maxChars?: number } = {},
|
|
86
|
-
): WordGroup[] {
|
|
87
|
-
if (words.length === 0) return [];
|
|
88
|
-
|
|
89
|
-
const targetWords = options.wordsPerGroup ?? 4;
|
|
90
|
-
const maxChars = options.maxChars ?? 40;
|
|
91
|
-
const groups: WordGroup[] = [];
|
|
92
|
-
let current: WordTiming[] = [];
|
|
93
|
-
|
|
94
|
-
function flush() {
|
|
95
|
-
if (current.length === 0) return;
|
|
96
|
-
groups.push({
|
|
97
|
-
words: [...current],
|
|
98
|
-
startTime: current[0].start,
|
|
99
|
-
endTime: current[current.length - 1].end,
|
|
100
|
-
text: current.map((w) => w.word).join(" "),
|
|
101
|
-
});
|
|
102
|
-
current = [];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
for (let i = 0; i < words.length; i++) {
|
|
106
|
-
const word = words[i];
|
|
107
|
-
current.push(word);
|
|
108
|
-
|
|
109
|
-
const currentText = current.map((w) => w.word).join(" ");
|
|
110
|
-
const nextWord = words[i + 1];
|
|
111
|
-
|
|
112
|
-
// Force split: max chars exceeded
|
|
113
|
-
if (currentText.length >= maxChars) {
|
|
114
|
-
flush();
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Sentence-ending punctuation
|
|
119
|
-
if (SENTENCE_BREAKS.test(word.word)) {
|
|
120
|
-
flush();
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Long pause before next word
|
|
125
|
-
if (nextWord && nextWord.start - word.end > LONG_PAUSE_THRESHOLD) {
|
|
126
|
-
flush();
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Clause break at target word count
|
|
131
|
-
if (current.length >= targetWords && CLAUSE_BREAKS.test(word.word)) {
|
|
132
|
-
flush();
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Reached target + 1 words without a break — flush at target
|
|
137
|
-
if (current.length >= targetWords + 1) {
|
|
138
|
-
flush();
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
flush();
|
|
144
|
-
return groups;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ── ASS Subtitle Generator ───────────────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
function colorToASS(hex: string): string {
|
|
150
|
-
// Convert #RRGGBB to &HBBGGRR& (ASS format)
|
|
151
|
-
const r = hex.slice(1, 3);
|
|
152
|
-
const g = hex.slice(3, 5);
|
|
153
|
-
const b = hex.slice(5, 7);
|
|
154
|
-
return `&H00${b}${g}${r}&`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function secondsToCentiseconds(s: number): number {
|
|
158
|
-
return Math.round(s * 100);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function formatASSTime(seconds: number): string {
|
|
162
|
-
const h = Math.floor(seconds / 3600);
|
|
163
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
164
|
-
const s = seconds % 60;
|
|
165
|
-
return `${h}:${String(m).padStart(2, "0")}:${s.toFixed(2).padStart(5, "0")}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Generate ASS subtitle content for fast-tier animated captions.
|
|
170
|
-
*/
|
|
171
|
-
export function generateASS(
|
|
172
|
-
groups: WordGroup[],
|
|
173
|
-
style: "karaoke-sweep" | "typewriter",
|
|
174
|
-
options: {
|
|
175
|
-
highlightColor: string;
|
|
176
|
-
fontSize: number;
|
|
177
|
-
position: string;
|
|
178
|
-
width: number;
|
|
179
|
-
height: number;
|
|
180
|
-
},
|
|
181
|
-
): string {
|
|
182
|
-
const assColor = colorToASS(options.highlightColor);
|
|
183
|
-
// ASS alignment: 8 = top-center, 5 = center, 2 = bottom-center
|
|
184
|
-
const alignment = options.position === "top" ? 8 : options.position === "center" ? 5 : 2;
|
|
185
|
-
const marginV = options.position === "center" ? 0 : 40;
|
|
186
|
-
|
|
187
|
-
const header = `[Script Info]
|
|
188
|
-
Title: Animated Captions
|
|
189
|
-
ScriptType: v4.00+
|
|
190
|
-
PlayResX: ${options.width}
|
|
191
|
-
PlayResY: ${options.height}
|
|
192
|
-
WrapStyle: 0
|
|
193
|
-
|
|
194
|
-
[V4+ Styles]
|
|
195
|
-
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
196
|
-
Style: Default,Arial,${options.fontSize},&H00FFFFFF,${assColor},&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,2,1,${alignment},20,20,${marginV},1
|
|
197
|
-
|
|
198
|
-
[Events]
|
|
199
|
-
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
200
|
-
`;
|
|
201
|
-
|
|
202
|
-
const events: string[] = [];
|
|
203
|
-
|
|
204
|
-
for (const group of groups) {
|
|
205
|
-
const start = formatASSTime(group.startTime);
|
|
206
|
-
const end = formatASSTime(group.endTime);
|
|
207
|
-
|
|
208
|
-
if (style === "karaoke-sweep") {
|
|
209
|
-
// Build karaoke tags: \kf<duration_cs> for each word
|
|
210
|
-
let text = "";
|
|
211
|
-
for (const word of group.words) {
|
|
212
|
-
const durationCs = secondsToCentiseconds(word.end - word.start);
|
|
213
|
-
text += `{\\kf${durationCs}}${word.word} `;
|
|
214
|
-
}
|
|
215
|
-
events.push(`Dialogue: 0,${start},${end},Default,,0,0,0,,${text.trim()}`);
|
|
216
|
-
} else {
|
|
217
|
-
// typewriter: each word fades in sequentially
|
|
218
|
-
for (let i = 0; i < group.words.length; i++) {
|
|
219
|
-
const word = group.words[i];
|
|
220
|
-
const wordStart = formatASSTime(word.start);
|
|
221
|
-
// Show all accumulated words up to this point
|
|
222
|
-
const accumulatedText = group.words
|
|
223
|
-
.slice(0, i + 1)
|
|
224
|
-
.map((w) => w.word)
|
|
225
|
-
.join(" ");
|
|
226
|
-
const fadeMs = 100;
|
|
227
|
-
events.push(
|
|
228
|
-
`Dialogue: 0,${wordStart},${end},Default,,0,0,0,,{\\fad(${fadeMs},0)}${accumulatedText}`,
|
|
229
|
-
);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return header + events.join("\n") + "\n";
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Execute Function ──────────────────────────────────────────────────────
|
|
238
|
-
|
|
239
|
-
export async function executeAnimatedCaption(
|
|
240
|
-
options: AnimatedCaptionOptions,
|
|
241
|
-
): Promise<AnimatedCaptionResult> {
|
|
242
|
-
const {
|
|
243
|
-
videoPath,
|
|
244
|
-
outputPath,
|
|
245
|
-
style,
|
|
246
|
-
highlightColor,
|
|
247
|
-
fontSize,
|
|
248
|
-
position,
|
|
249
|
-
wordsPerGroup,
|
|
250
|
-
maxChars,
|
|
251
|
-
language,
|
|
252
|
-
fast,
|
|
253
|
-
} = options;
|
|
254
|
-
|
|
255
|
-
// Determine tier
|
|
256
|
-
const isASSTier = fast || ASS_STYLES.includes(style);
|
|
257
|
-
const effectiveStyle = isASSTier && !ASS_STYLES.includes(style) ? "karaoke-sweep" : style;
|
|
258
|
-
const tier = isASSTier ? "ass" : "remotion";
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
// 1. Get video info
|
|
262
|
-
const [dims, duration] = await Promise.all([
|
|
263
|
-
ffprobeVideoSize(videoPath),
|
|
264
|
-
ffprobeDuration(videoPath),
|
|
265
|
-
]);
|
|
266
|
-
const width = dims.width;
|
|
267
|
-
const height = dims.height;
|
|
268
|
-
|
|
269
|
-
// Get FPS via ffprobe
|
|
270
|
-
let videoFps = 30;
|
|
271
|
-
try {
|
|
272
|
-
const { stdout: fpsOut } = await execSafe("ffprobe", [
|
|
273
|
-
"-v", "error", "-select_streams", "v:0",
|
|
274
|
-
"-show_entries", "stream=r_frame_rate",
|
|
275
|
-
"-of", "csv=p=0", videoPath,
|
|
276
|
-
]);
|
|
277
|
-
const [num, den] = fpsOut.trim().split("/").map(Number);
|
|
278
|
-
if (num && den) videoFps = Math.round(num / den);
|
|
279
|
-
} catch {
|
|
280
|
-
// fallback to 30 fps
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Auto font size: ~4% of height
|
|
284
|
-
const effectiveFontSize = fontSize ?? Math.round(height * 0.04);
|
|
285
|
-
|
|
286
|
-
// 2. Extract audio
|
|
287
|
-
const tmpAudioDir = resolve(tmpdir(), `vf-ac-${Date.now()}`);
|
|
288
|
-
await mkdir(tmpAudioDir, { recursive: true });
|
|
289
|
-
const audioPath = resolve(tmpAudioDir, "audio.wav");
|
|
290
|
-
|
|
291
|
-
await execSafe("ffmpeg", [
|
|
292
|
-
"-y", "-i", videoPath,
|
|
293
|
-
"-vn", "-acodec", "pcm_s16le", "-ar", "16000", "-ac", "1",
|
|
294
|
-
audioPath,
|
|
295
|
-
], { timeout: 120_000 });
|
|
296
|
-
|
|
297
|
-
// 3. Transcribe with word-level timestamps
|
|
298
|
-
const apiKey = await getApiKey("OPENAI_API_KEY", "OpenAI");
|
|
299
|
-
if (!apiKey) {
|
|
300
|
-
await rm(tmpAudioDir, { recursive: true, force: true }).catch(() => {});
|
|
301
|
-
return { success: false, error: "OPENAI_API_KEY required for Whisper transcription. Run 'vibe setup' or set OPENAI_API_KEY in .env" };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const transcript = await transcribeWithWords(audioPath, apiKey, language);
|
|
305
|
-
if (!transcript.words || transcript.words.length === 0) {
|
|
306
|
-
await rm(tmpAudioDir, { recursive: true, force: true }).catch(() => {});
|
|
307
|
-
return { success: false, error: "No words detected in transcription" };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// 4. Group words
|
|
311
|
-
const groups = groupWords(transcript.words, { wordsPerGroup, maxChars });
|
|
312
|
-
|
|
313
|
-
// 5. Route by tier
|
|
314
|
-
const absOutputPath = resolve(process.cwd(), outputPath);
|
|
315
|
-
const outDir = dirname(absOutputPath);
|
|
316
|
-
if (!existsSync(outDir)) {
|
|
317
|
-
await mkdir(outDir, { recursive: true });
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (tier === "ass") {
|
|
321
|
-
// ASS tier: generate .ass file → FFmpeg subtitles filter
|
|
322
|
-
const assContent = generateASS(
|
|
323
|
-
groups,
|
|
324
|
-
effectiveStyle as "karaoke-sweep" | "typewriter",
|
|
325
|
-
{ highlightColor, fontSize: effectiveFontSize, position, width, height },
|
|
326
|
-
);
|
|
327
|
-
const assPath = resolve(tmpAudioDir, "captions.ass");
|
|
328
|
-
await writeFile(assPath, assContent, "utf-8");
|
|
329
|
-
|
|
330
|
-
// Escape path for FFmpeg subtitles filter (colon and backslash)
|
|
331
|
-
const escapedAssPath = assPath.replace(/\\/g, "\\\\").replace(/:/g, "\\:");
|
|
332
|
-
|
|
333
|
-
await execSafe("ffmpeg", [
|
|
334
|
-
"-y", "-i", videoPath,
|
|
335
|
-
"-vf", `ass=${escapedAssPath}`,
|
|
336
|
-
"-c:a", "copy",
|
|
337
|
-
absOutputPath,
|
|
338
|
-
], { timeout: 300_000 });
|
|
339
|
-
} else {
|
|
340
|
-
// Remotion tier: generate component → render with embedded video
|
|
341
|
-
const component = generateAnimatedCaptionComponent({
|
|
342
|
-
groups,
|
|
343
|
-
style: effectiveStyle as "highlight" | "bounce" | "pop-in" | "neon",
|
|
344
|
-
highlightColor,
|
|
345
|
-
fontSize: effectiveFontSize,
|
|
346
|
-
position,
|
|
347
|
-
width,
|
|
348
|
-
height,
|
|
349
|
-
fps: videoFps,
|
|
350
|
-
videoFileName: basename(videoPath),
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const durationInFrames = Math.ceil(duration * videoFps);
|
|
354
|
-
|
|
355
|
-
const renderResult = await renderWithEmbeddedVideo({
|
|
356
|
-
componentCode: component.code,
|
|
357
|
-
componentName: component.name,
|
|
358
|
-
width,
|
|
359
|
-
height,
|
|
360
|
-
fps: videoFps,
|
|
361
|
-
durationInFrames,
|
|
362
|
-
videoPath,
|
|
363
|
-
videoFileName: basename(videoPath),
|
|
364
|
-
outputPath: absOutputPath,
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
if (!renderResult.success) {
|
|
368
|
-
await rm(tmpAudioDir, { recursive: true, force: true }).catch(() => {});
|
|
369
|
-
return { success: false, error: renderResult.error };
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Cleanup
|
|
374
|
-
await rm(tmpAudioDir, { recursive: true, force: true }).catch(() => {});
|
|
375
|
-
|
|
376
|
-
return {
|
|
377
|
-
success: true,
|
|
378
|
-
outputPath: absOutputPath,
|
|
379
|
-
wordCount: transcript.words.length,
|
|
380
|
-
groupCount: groups.length,
|
|
381
|
-
style: effectiveStyle,
|
|
382
|
-
tier,
|
|
383
|
-
};
|
|
384
|
-
} catch (error) {
|
|
385
|
-
return {
|
|
386
|
-
success: false,
|
|
387
|
-
error: `Animated caption failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
}
|