@vibeframe/cli 0.27.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/adapters/index.d.ts +1 -0
  3. package/dist/agent/adapters/index.d.ts.map +1 -1
  4. package/dist/agent/adapters/index.js +5 -0
  5. package/dist/agent/adapters/index.js.map +1 -1
  6. package/dist/agent/adapters/openrouter.d.ts +16 -0
  7. package/dist/agent/adapters/openrouter.d.ts.map +1 -0
  8. package/dist/agent/adapters/openrouter.js +100 -0
  9. package/dist/agent/adapters/openrouter.js.map +1 -0
  10. package/dist/agent/types.d.ts +1 -1
  11. package/dist/agent/types.d.ts.map +1 -1
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +3 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/ai-edit-cli.d.ts.map +1 -1
  16. package/dist/commands/ai-edit-cli.js +18 -0
  17. package/dist/commands/ai-edit-cli.js.map +1 -1
  18. package/dist/commands/generate.js +14 -0
  19. package/dist/commands/generate.js.map +1 -1
  20. package/dist/commands/schema.d.ts +1 -0
  21. package/dist/commands/schema.d.ts.map +1 -1
  22. package/dist/commands/schema.js +122 -21
  23. package/dist/commands/schema.js.map +1 -1
  24. package/dist/commands/setup.js +5 -2
  25. package/dist/commands/setup.js.map +1 -1
  26. package/dist/config/schema.d.ts +2 -1
  27. package/dist/config/schema.d.ts.map +1 -1
  28. package/dist/config/schema.js +2 -0
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/index.js +0 -0
  31. package/package.json +16 -12
  32. package/.turbo/turbo-build.log +0 -4
  33. package/.turbo/turbo-lint.log +0 -21
  34. package/.turbo/turbo-test.log +0 -689
  35. package/src/agent/adapters/claude.ts +0 -143
  36. package/src/agent/adapters/gemini.ts +0 -159
  37. package/src/agent/adapters/index.ts +0 -61
  38. package/src/agent/adapters/ollama.ts +0 -231
  39. package/src/agent/adapters/openai.ts +0 -116
  40. package/src/agent/adapters/xai.ts +0 -119
  41. package/src/agent/index.ts +0 -251
  42. package/src/agent/memory/index.ts +0 -151
  43. package/src/agent/prompts/system.ts +0 -106
  44. package/src/agent/tools/ai-editing.ts +0 -845
  45. package/src/agent/tools/ai-generation.ts +0 -1073
  46. package/src/agent/tools/ai-pipeline.ts +0 -1055
  47. package/src/agent/tools/ai.ts +0 -21
  48. package/src/agent/tools/batch.ts +0 -429
  49. package/src/agent/tools/e2e.test.ts +0 -545
  50. package/src/agent/tools/export.ts +0 -184
  51. package/src/agent/tools/filesystem.ts +0 -237
  52. package/src/agent/tools/index.ts +0 -150
  53. package/src/agent/tools/integration.test.ts +0 -775
  54. package/src/agent/tools/media.ts +0 -697
  55. package/src/agent/tools/project.ts +0 -313
  56. package/src/agent/tools/timeline.ts +0 -951
  57. package/src/agent/types.ts +0 -68
  58. package/src/commands/agent.ts +0 -340
  59. package/src/commands/ai-analyze.ts +0 -429
  60. package/src/commands/ai-animated-caption.ts +0 -390
  61. package/src/commands/ai-audio.ts +0 -941
  62. package/src/commands/ai-broll.ts +0 -490
  63. package/src/commands/ai-edit-cli.ts +0 -658
  64. package/src/commands/ai-edit.ts +0 -1542
  65. package/src/commands/ai-fill-gaps.ts +0 -566
  66. package/src/commands/ai-helpers.ts +0 -65
  67. package/src/commands/ai-highlights.ts +0 -1303
  68. package/src/commands/ai-image.ts +0 -761
  69. package/src/commands/ai-motion.ts +0 -347
  70. package/src/commands/ai-narrate.ts +0 -451
  71. package/src/commands/ai-review.ts +0 -309
  72. package/src/commands/ai-script-pipeline-cli.ts +0 -1710
  73. package/src/commands/ai-script-pipeline.ts +0 -1365
  74. package/src/commands/ai-suggest-edit.ts +0 -264
  75. package/src/commands/ai-video-fx.ts +0 -445
  76. package/src/commands/ai-video.ts +0 -915
  77. package/src/commands/ai-viral.ts +0 -595
  78. package/src/commands/ai-visual-fx.ts +0 -601
  79. package/src/commands/ai.test.ts +0 -627
  80. package/src/commands/ai.ts +0 -307
  81. package/src/commands/analyze.ts +0 -282
  82. package/src/commands/audio.ts +0 -644
  83. package/src/commands/batch.test.ts +0 -279
  84. package/src/commands/batch.ts +0 -440
  85. package/src/commands/detect.ts +0 -329
  86. package/src/commands/doctor.ts +0 -237
  87. package/src/commands/edit-cmd.ts +0 -1014
  88. package/src/commands/export.ts +0 -918
  89. package/src/commands/generate.ts +0 -2146
  90. package/src/commands/media.ts +0 -177
  91. package/src/commands/output.ts +0 -142
  92. package/src/commands/pipeline.ts +0 -398
  93. package/src/commands/project.test.ts +0 -127
  94. package/src/commands/project.ts +0 -149
  95. package/src/commands/sanitize.ts +0 -60
  96. package/src/commands/schema.ts +0 -130
  97. package/src/commands/setup.ts +0 -509
  98. package/src/commands/timeline.test.ts +0 -499
  99. package/src/commands/timeline.ts +0 -529
  100. package/src/commands/validate.ts +0 -77
  101. package/src/config/config.test.ts +0 -197
  102. package/src/config/index.ts +0 -125
  103. package/src/config/schema.ts +0 -82
  104. package/src/engine/index.ts +0 -2
  105. package/src/engine/project.test.ts +0 -702
  106. package/src/engine/project.ts +0 -439
  107. package/src/index.ts +0 -146
  108. package/src/utils/api-key.test.ts +0 -41
  109. package/src/utils/api-key.ts +0 -247
  110. package/src/utils/audio.ts +0 -83
  111. package/src/utils/exec-safe.ts +0 -75
  112. package/src/utils/first-run.ts +0 -52
  113. package/src/utils/provider-resolver.ts +0 -56
  114. package/src/utils/remotion.ts +0 -951
  115. package/src/utils/subtitle.test.ts +0 -227
  116. package/src/utils/subtitle.ts +0 -169
  117. package/src/utils/tty.ts +0 -196
  118. 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
- }