@vibeframe/mcp-server 0.71.0 → 0.73.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 (3) hide show
  1. package/README.md +38 -10
  2. package/dist/index.js +1587 -986
  3. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -7330,6 +7330,441 @@ var require_dist = __commonJS({
7330
7330
  }
7331
7331
  });
7332
7332
 
7333
+ // ../cli/src/commands/_shared/scene-project.ts
7334
+ import { mkdir, readFile, writeFile, access } from "node:fs/promises";
7335
+ import { resolve, basename } from "node:path";
7336
+ function aspectToDims(aspect) {
7337
+ return ASPECT_DIMS[aspect];
7338
+ }
7339
+ function defaultVibeProjectConfig(name) {
7340
+ return {
7341
+ name,
7342
+ aspect: "16:9",
7343
+ defaultSceneDuration: 5,
7344
+ providers: { image: null, tts: null, transcribe: null },
7345
+ budget: { maxUsd: 0 }
7346
+ };
7347
+ }
7348
+ function buildHyperframesConfig() {
7349
+ return {
7350
+ $schema: "https://hyperframes.heygen.com/schema/hyperframes.json",
7351
+ registry: "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
7352
+ paths: {
7353
+ blocks: "compositions",
7354
+ components: "compositions/components",
7355
+ assets: "assets"
7356
+ }
7357
+ };
7358
+ }
7359
+ function buildHyperframesMeta(name, now = /* @__PURE__ */ new Date()) {
7360
+ return { id: name, name, createdAt: now.toISOString() };
7361
+ }
7362
+ function mergeHyperframesConfig(existing, defaults) {
7363
+ const out = { ...defaults, ...existing };
7364
+ if (existing.paths || defaults.paths) {
7365
+ out.paths = { ...defaults.paths ?? {}, ...existing.paths ?? {} };
7366
+ }
7367
+ return out;
7368
+ }
7369
+ function buildEmptyRootHtml(opts) {
7370
+ const { width, height } = ASPECT_DIMS[opts.aspect];
7371
+ return `<!doctype html>
7372
+ <html lang="en">
7373
+ <head>
7374
+ <meta charset="UTF-8" />
7375
+ <meta name="viewport" content="width=${width}, height=${height}" />
7376
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
7377
+ <style>
7378
+ * { margin: 0; padding: 0; box-sizing: border-box; }
7379
+ html, body {
7380
+ margin: 0;
7381
+ width: ${width}px;
7382
+ height: ${height}px;
7383
+ overflow: hidden;
7384
+ background: #000;
7385
+ }
7386
+ </style>
7387
+ </head>
7388
+ <body>
7389
+ <div
7390
+ id="root"
7391
+ data-composition-id="main"
7392
+ data-start="0"
7393
+ data-duration="${opts.duration}"
7394
+ data-width="${width}"
7395
+ data-height="${height}"
7396
+ >
7397
+ <!-- Scenes added via \`vibe scene add\` are inserted here. -->
7398
+ <!-- Each scene reference: data-composition-id, data-composition-src, data-start, data-duration, data-track-index. -->
7399
+ <!-- See compositions/*.html for sub-composition contents. -->
7400
+
7401
+ </div>
7402
+
7403
+ <script>
7404
+ window.__timelines = window.__timelines || {};
7405
+ window.__timelines["main"] = gsap.timeline({ paused: true });
7406
+ </script>
7407
+ </body>
7408
+ </html>
7409
+ `;
7410
+ }
7411
+ function buildDesignMd(opts) {
7412
+ const { name, style } = opts;
7413
+ const intro = style ? `Visual identity for **${name}**, scaffolded from the **${style.name}** style (after ${style.designer}). Customise freely \u2014 this file is the single source of truth for every scene's palette, typography, and motion.` : `Visual identity for **${name}**. Fill the sections below before authoring any scene HTML or generating any backdrop. Pick a named style with \`vibe scene styles\` if you want a credible starting point.`;
7414
+ const moodLine = style ? `**Mood:** ${style.mood} \xB7 **Best for:** ${style.bestFor}` : `**Mood:** _(one line \u2014 what should the viewer FEEL?)_`;
7415
+ const palette = style ? `${style.palette.map((c) => `- \`${c}\``).join("\n")}
7416
+
7417
+ ${style.paletteNotes}` : `- _hex_ \u2014 primary
7418
+ - _hex_ \u2014 accent
7419
+
7420
+ _2\u20133 colours max. Declare explicit hex values; never name colours abstractly._`;
7421
+ const typography = style ? style.typography : `_One family, two weights. State the role of each (headline / label / body)._`;
7422
+ const composition = style ? style.composition : `_Grid? Centered? Layered? How does negative space behave?_`;
7423
+ const motion = style ? `${style.motion}
7424
+
7425
+ **GSAP signature:** ${style.gsapSignature}` : `_How fast? Snappy or fluid? Overshoot or precision?_
7426
+
7427
+ **GSAP signature:** _e.g. \`expo.out\`, \`sine.inOut\`, \`back.out(1.8)\`_`;
7428
+ const transition = style ? style.transition : `_Which Hyperframes shader matches the energy? (Cinematic Zoom, Cross-Warp Morph, Glitch, Domain Warp, \u2026)_`;
7429
+ const avoid = style ? style.avoid.map((a) => `- ${a}`).join("\n") : `- _anti-pattern 1_
7430
+ - _anti-pattern 2_
7431
+ - _anti-pattern 3_`;
7432
+ return `# ${name} \u2014 Design
7433
+
7434
+ > **Hard-gate.** This file defines the visual identity of every scene.
7435
+ > Author it before generating any HTML, backdrop image, or motion.
7436
+ > The Hyperframes \`hyperframes\` skill enforces this: scenes that
7437
+ > contradict DESIGN.md are rejected.
7438
+
7439
+ ${intro}
7440
+
7441
+ ## Style
7442
+
7443
+ ${moodLine}
7444
+
7445
+ ## Palette
7446
+
7447
+ ${palette}
7448
+
7449
+ ## Typography
7450
+
7451
+ ${typography}
7452
+
7453
+ ## Composition
7454
+
7455
+ ${composition}
7456
+
7457
+ ## Motion
7458
+
7459
+ ${motion}
7460
+
7461
+ ## Transition
7462
+
7463
+ ${transition}
7464
+
7465
+ ## What NOT to do
7466
+
7467
+ ${avoid}
7468
+
7469
+ ---
7470
+
7471
+ _Browse other named styles: \`vibe scene styles\`_
7472
+ ${style ? `_This file was seeded by \`vibe scene init --visual-style "${style.name}"\`._` : `_Seed this file from a named style: \`vibe scene init <dir> --visual-style "<name>"\`._`}
7473
+ `;
7474
+ }
7475
+ function buildStoryboardMd(name, duration = 12) {
7476
+ return `---
7477
+ title: ${name}
7478
+ duration: ${duration}
7479
+ aspect: 16:9
7480
+ tts: auto
7481
+ imageProvider: openai
7482
+ ---
7483
+
7484
+ # ${name} \u2014 Storyboard
7485
+
7486
+ Edit these beats before running \`vibe build\`. Each beat starts with
7487
+ YAML cues that drive narration, backdrop generation, and timing.
7488
+
7489
+ ## Beat hook \u2014 Hook
7490
+
7491
+ \`\`\`yaml
7492
+ narration: "Introduce the promise in one crisp sentence."
7493
+ backdrop: "Cinematic abstract technology backdrop, precise light, premium editorial feel"
7494
+ duration: 4
7495
+ \`\`\`
7496
+
7497
+ Show the core visual identity immediately. Keep copy short enough for one
7498
+ screen and one spoken breath.
7499
+
7500
+ ## Beat proof \u2014 Proof
7501
+
7502
+ \`\`\`yaml
7503
+ narration: "Show the mechanism or proof point that makes the promise believable."
7504
+ backdrop: "Layered interface details, subtle motion trails, high-contrast product storytelling"
7505
+ duration: 4
7506
+ \`\`\`
7507
+
7508
+ Use this beat for the concrete differentiator: command, workflow, metric, or
7509
+ before/after.
7510
+
7511
+ ## Beat close \u2014 Close
7512
+
7513
+ \`\`\`yaml
7514
+ narration: "Close with the action the viewer should remember."
7515
+ backdrop: "Resolved hero frame, confident final composition, clean negative space"
7516
+ duration: 4
7517
+ \`\`\`
7518
+
7519
+ End on the product name, offer, or command. Avoid adding a new idea in the
7520
+ final beat.
7521
+ `;
7522
+ }
7523
+ function buildProjectClaudeMd(name) {
7524
+ return `# ${name} \u2014 Scene Authoring Project
7525
+
7526
+ This project is **bilingual**: it works with both VibeFrame (\`vibe\`) and
7527
+ HeyGen Hyperframes (\`hyperframes\`). You can run either CLI inside this
7528
+ directory.
7529
+
7530
+ ## Visual identity hard-gate
7531
+
7532
+ **Author \`DESIGN.md\` before any scene HTML.** It defines palette,
7533
+ typography, motion, and transition rules. Both the agent-driven path and
7534
+ the fallback emit reference it; scenes that contradict DESIGN.md are
7535
+ rejected by the Hyperframes \`hyperframes\` skill.
7536
+
7537
+ Browse named styles: \`vibe scene styles\`. Re-seed from one with
7538
+ \`vibe scene init . --visual-style "Swiss Pulse"\` (idempotent).
7539
+
7540
+ ## Skills \u2014 USE THESE FIRST
7541
+
7542
+ **Always invoke the relevant skill before authoring scenes.** Skills encode
7543
+ framework-specific patterns (GSAP timeline registration, data-attribute
7544
+ semantics, VibeFrame pipeline conventions) that are NOT in generic web docs.
7545
+
7546
+ | Skill | Command | When to use |
7547
+ | ----------------- | ---------------- | ------------------------------------------------------------------------------------- |
7548
+ | **hyperframes** | \`/hyperframes\` | Cinematic-quality composition \u2014 DESIGN.md hard-gate, named styles, motion principles |
7549
+ | **vibe-scene** | \`/vibe-scene\` | VibeFrame's authoring loop, AI assets, lint feedback, pipeline integration |
7550
+ | **gsap** | \`/gsap\` | GSAP tweens, timelines, easing |
7551
+
7552
+ Optional: install the upstream Hyperframes skills once per machine when your agent supports skill commands:
7553
+
7554
+ \`\`\`bash
7555
+ npx skills add heygen-com/hyperframes
7556
+ \`\`\`
7557
+
7558
+ Restart your agent session (or reload the skill list) after installing.
7559
+ If skills aren't available, follow the **Key Rules** below \u2014 they cover
7560
+ the framework-level minimum, not the cinematic craft layer.
7561
+
7562
+ ## Project structure
7563
+
7564
+ - \`DESIGN.md\` \u2014 visual identity contract (palette, type, motion, transitions)
7565
+ - \`STORYBOARD.md\` \u2014 per-beat narration/backdrop/duration cues for \`vibe build\`
7566
+ - \`index.html\` \u2014 root composition (timeline)
7567
+ - \`compositions/scene-*.html\` \u2014 per-scene HTML authored by you or the agent
7568
+ - \`assets/\` \u2014 shared media (narration audio, images, video)
7569
+ - \`transcript.json\` \u2014 Whisper word-level transcript (if narration exists)
7570
+ - \`hyperframes.json\` \u2014 HF registry config (speak to both toolchains)
7571
+ - \`vibe.project.yaml\` \u2014 VibeFrame config (providers, budget)
7572
+ - \`renders/\` \u2014 output MP4s
7573
+
7574
+ ## Commands
7575
+
7576
+ \`\`\`bash
7577
+ vibe scene add <name> --narration "..." --visuals "..." # Author a new scene via AI
7578
+ vibe build # STORYBOARD.md \u2192 narrated MP4
7579
+ vibe scene lint # Validate scenes (in-process HF linter)
7580
+ vibe scene render # Render to MP4
7581
+
7582
+ # Hyperframes CLI (if installed \u2014 works in this project too)
7583
+ npx hyperframes preview
7584
+ npx hyperframes render
7585
+ \`\`\`
7586
+
7587
+ ## Key Rules (for hand-authored scene HTML)
7588
+
7589
+ 1. Every timed element needs \`data-start\`, \`data-duration\`, and \`data-track-index\`.
7590
+ 2. Elements with timing **MUST** have \`class="clip"\` \u2014 the framework uses this for visibility control.
7591
+ 3. Timelines must be paused and registered on \`window.__timelines\`:
7592
+ \`\`\`js
7593
+ window.__timelines = window.__timelines || {};
7594
+ window.__timelines["composition-id"] = gsap.timeline({ paused: true });
7595
+ \`\`\`
7596
+ 4. Videos use \`muted\` with a separate \`<audio>\` element for the audio track.
7597
+ 5. Sub-compositions use \`data-composition-src="compositions/file.html"\`.
7598
+ 6. Only deterministic logic \u2014 no \`Date.now()\`, \`Math.random()\`, or network fetches.
7599
+
7600
+ ## Linting \u2014 run after changes
7601
+
7602
+ \`\`\`bash
7603
+ vibe scene lint # preferred \u2014 in-process, no network
7604
+ vibe scene lint --fix # auto-fix mechanical issues
7605
+ vibe scene lint --json # structured output for agent loops
7606
+ \`\`\`
7607
+ `;
7608
+ }
7609
+ function buildSceneGitignore() {
7610
+ return `# VibeFrame caches
7611
+ .vibeframe/cache/
7612
+ .vibeframe/checkpoints/
7613
+
7614
+ # Render outputs
7615
+ renders/*.mp4
7616
+ tmp/
7617
+
7618
+ # OS / editor
7619
+ .DS_Store
7620
+ *.log
7621
+ `;
7622
+ }
7623
+ function isSceneScaffoldProfile(value) {
7624
+ return value === "minimal" || value === "agent" || value === "full";
7625
+ }
7626
+ function describeSceneScaffold(opts) {
7627
+ const dir = resolve(opts.dir);
7628
+ const profile = opts.profile ?? "full";
7629
+ const groups = {
7630
+ authoring: [
7631
+ resolve(dir, "STORYBOARD.md"),
7632
+ resolve(dir, "DESIGN.md"),
7633
+ resolve(dir, "vibe.project.yaml"),
7634
+ resolve(dir, ".gitignore")
7635
+ ],
7636
+ render: [],
7637
+ agent: []
7638
+ };
7639
+ if (profile === "full") {
7640
+ groups.render = [
7641
+ resolve(dir, "index.html"),
7642
+ resolve(dir, "compositions"),
7643
+ resolve(dir, "assets"),
7644
+ resolve(dir, "renders"),
7645
+ resolve(dir, "hyperframes.json"),
7646
+ resolve(dir, "meta.json")
7647
+ ];
7648
+ }
7649
+ if (profile === "agent" || profile === "full") {
7650
+ groups.agent = [
7651
+ resolve(dir, "SKILL.md"),
7652
+ resolve(dir, "references"),
7653
+ resolve(dir, "CLAUDE.md")
7654
+ ];
7655
+ }
7656
+ return groups;
7657
+ }
7658
+ async function pathExists(p) {
7659
+ try {
7660
+ await access(p);
7661
+ return true;
7662
+ } catch {
7663
+ return false;
7664
+ }
7665
+ }
7666
+ async function scaffoldSceneProject(opts) {
7667
+ const dir = resolve(opts.dir);
7668
+ const name = opts.name ?? basename(dir);
7669
+ const aspect = opts.aspect ?? "16:9";
7670
+ const duration = opts.duration ?? 10;
7671
+ const now = opts.now ?? /* @__PURE__ */ new Date();
7672
+ const profile = opts.profile ?? "full";
7673
+ await mkdir(dir, { recursive: true });
7674
+ if (profile === "full") {
7675
+ await mkdir(resolve(dir, "compositions"), { recursive: true });
7676
+ await mkdir(resolve(dir, "assets"), { recursive: true });
7677
+ await mkdir(resolve(dir, "renders"), { recursive: true });
7678
+ }
7679
+ const created = [];
7680
+ const skipped2 = [];
7681
+ const merged = [];
7682
+ if (profile === "full") {
7683
+ const hfPath = resolve(dir, "hyperframes.json");
7684
+ const hfDefaults = buildHyperframesConfig();
7685
+ if (await pathExists(hfPath)) {
7686
+ const existingRaw = await readFile(hfPath, "utf-8");
7687
+ const existing = JSON.parse(existingRaw);
7688
+ const mergedConfig = mergeHyperframesConfig(existing, hfDefaults);
7689
+ await writeFile(hfPath, JSON.stringify(mergedConfig, null, 2) + "\n", "utf-8");
7690
+ merged.push(hfPath);
7691
+ } else {
7692
+ await writeFile(hfPath, JSON.stringify(hfDefaults, null, 2) + "\n", "utf-8");
7693
+ created.push(hfPath);
7694
+ }
7695
+ const metaPath = resolve(dir, "meta.json");
7696
+ if (await pathExists(metaPath)) {
7697
+ skipped2.push(metaPath);
7698
+ } else {
7699
+ await writeFile(metaPath, JSON.stringify(buildHyperframesMeta(name, now), null, 2) + "\n", "utf-8");
7700
+ created.push(metaPath);
7701
+ }
7702
+ const rootPath = resolve(dir, "index.html");
7703
+ if (await pathExists(rootPath)) {
7704
+ skipped2.push(rootPath);
7705
+ } else {
7706
+ await writeFile(rootPath, buildEmptyRootHtml({ aspect, duration }), "utf-8");
7707
+ created.push(rootPath);
7708
+ }
7709
+ }
7710
+ const vibePath = resolve(dir, "vibe.project.yaml");
7711
+ if (await pathExists(vibePath)) {
7712
+ skipped2.push(vibePath);
7713
+ } else {
7714
+ const cfg = { ...defaultVibeProjectConfig(name), aspect };
7715
+ await writeFile(vibePath, (0, import_yaml.stringify)(cfg), "utf-8");
7716
+ created.push(vibePath);
7717
+ }
7718
+ if (profile === "agent" || profile === "full") {
7719
+ const claudePath = resolve(dir, "CLAUDE.md");
7720
+ if (await pathExists(claudePath)) {
7721
+ skipped2.push(claudePath);
7722
+ } else {
7723
+ await writeFile(claudePath, buildProjectClaudeMd(name), "utf-8");
7724
+ created.push(claudePath);
7725
+ }
7726
+ }
7727
+ const designPath = resolve(dir, "DESIGN.md");
7728
+ if (await pathExists(designPath)) {
7729
+ skipped2.push(designPath);
7730
+ } else {
7731
+ await writeFile(
7732
+ designPath,
7733
+ buildDesignMd({ name, style: opts.visualStyle }),
7734
+ "utf-8"
7735
+ );
7736
+ created.push(designPath);
7737
+ }
7738
+ const storyboardPath = resolve(dir, "STORYBOARD.md");
7739
+ if (await pathExists(storyboardPath)) {
7740
+ skipped2.push(storyboardPath);
7741
+ } else {
7742
+ await writeFile(storyboardPath, buildStoryboardMd(name, duration), "utf-8");
7743
+ created.push(storyboardPath);
7744
+ }
7745
+ const gitignorePath = resolve(dir, ".gitignore");
7746
+ if (await pathExists(gitignorePath)) {
7747
+ skipped2.push(gitignorePath);
7748
+ } else {
7749
+ await writeFile(gitignorePath, buildSceneGitignore(), "utf-8");
7750
+ created.push(gitignorePath);
7751
+ }
7752
+ return { created, skipped: skipped2, merged, groups: describeSceneScaffold({ dir, profile }) };
7753
+ }
7754
+ var import_yaml, ASPECT_DIMS;
7755
+ var init_scene_project = __esm({
7756
+ "../cli/src/commands/_shared/scene-project.ts"() {
7757
+ "use strict";
7758
+ import_yaml = __toESM(require_dist(), 1);
7759
+ ASPECT_DIMS = {
7760
+ "16:9": { width: 1920, height: 1080 },
7761
+ "9:16": { width: 1080, height: 1920 },
7762
+ "1:1": { width: 1080, height: 1080 },
7763
+ "4:5": { width: 1080, height: 1350 }
7764
+ };
7765
+ }
7766
+ });
7767
+
7333
7768
  // ../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/error.js
7334
7769
  var require_error = __commonJS({
7335
7770
  "../../node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/error.js"(exports) {
@@ -13713,12 +14148,12 @@ var init_api_keys = __esm({
13713
14148
  label: "ImgBB",
13714
14149
  showInSetup: false,
13715
14150
  // not prompted in setup wizard — internal upload host
13716
- envExampleComment: "ImgBB API Key (image hosting \u2014 used by Kling and fal.ai for image-to-video uploads)",
14151
+ envExampleComment: "ImgBB API Key (image hosting \u2014 used by Kling and Seedance for image-to-video uploads)",
13717
14152
  envExampleUrl: "https://api.imgbb.com/",
13718
14153
  // ImgBB has no provider class (envvar-only); doctor still shows what it
13719
14154
  // unlocks at the apiKey level.
13720
14155
  commandsUnlocked: [
13721
- "generate video -p kling/fal (image-to-video upload host)"
14156
+ "generate video -p kling/seedance (image-to-video upload host)"
13722
14157
  ]
13723
14158
  });
13724
14159
  defineProvider({
@@ -23787,13 +24222,18 @@ var init_fal = __esm({
23787
24222
  defineProvider({
23788
24223
  id: "fal",
23789
24224
  label: "fal.ai (Seedance 2.0)",
24225
+ displayName: "Seedance 2.0",
24226
+ gateway: "fal.ai",
24227
+ aliases: ["seedance"],
24228
+ models: ["seedance-2.0", "seedance-2.0-fast"],
24229
+ capabilities: ["text-to-video", "image-to-video", "native-audio"],
23790
24230
  apiKey: "fal",
23791
24231
  kinds: ["video"],
23792
24232
  resolverPriority: { video: 1 },
23793
24233
  commandsUnlocked: [
23794
- "generate video -p fal (Seedance 2.0 \u2014 default since v0.57)",
23795
- "generate video -p fal -m fast (lower-latency variant)",
23796
- "generate video -p fal -i <image> (image-to-video)"
24234
+ "generate video -p seedance (Seedance 2.0 via fal.ai \u2014 default since v0.57)",
24235
+ "generate video -p seedance --seedance-model fast (lower-latency variant)",
24236
+ "generate video -p seedance -i <image> (image-to-video)"
23797
24237
  ]
23798
24238
  });
23799
24239
  }
@@ -446848,7 +447288,9 @@ function buildAudioMuxFilter(audios) {
446848
447288
  const inputIdx = i + 1;
446849
447289
  const delayMs = Math.max(0, Math.round(a.absoluteStart * 1e3));
446850
447290
  const volume = Number.isFinite(a.volume) ? a.volume : 1;
446851
- const trimSec = Math.max(0, a.clipDurationCap);
447291
+ const durationHint = typeof a.durationHint === "number" && Number.isFinite(a.durationHint) ? Math.max(0, a.durationHint) : null;
447292
+ const clipCap = Math.max(0, a.clipDurationCap);
447293
+ const trimSec = durationHint === null ? clipCap : Math.min(durationHint, clipCap);
446852
447294
  const label = `a${i}`;
446853
447295
  const stage = [
446854
447296
  `[${inputIdx}:a]`,
@@ -447107,6 +447549,57 @@ var init_scene_render = __esm({
447107
447549
  }
447108
447550
  });
447109
447551
 
447552
+ // ../cli/src/utils/audio.ts
447553
+ async function getAudioDuration(filePath) {
447554
+ try {
447555
+ return await ffprobeDuration(filePath);
447556
+ } catch (error) {
447557
+ const message = error instanceof Error ? error.message : String(error);
447558
+ throw new Error(`Failed to get audio duration: ${message}`);
447559
+ }
447560
+ }
447561
+ async function getVideoDuration(filePath) {
447562
+ try {
447563
+ return await ffprobeDuration(filePath);
447564
+ } catch (error) {
447565
+ const message = error instanceof Error ? error.message : String(error);
447566
+ throw new Error(`Failed to get video duration: ${message}`);
447567
+ }
447568
+ }
447569
+ async function extendVideoNaturally(videoPath, targetDuration, outputPath) {
447570
+ const videoDuration = await getVideoDuration(videoPath);
447571
+ const ratio = targetDuration / videoDuration;
447572
+ if (ratio <= 1) {
447573
+ const { copyFile: copyFile5 } = await import("node:fs/promises");
447574
+ await copyFile5(videoPath, outputPath);
447575
+ return;
447576
+ }
447577
+ if (ratio <= 1.15) {
447578
+ const slowFactor = (1 / ratio).toFixed(4);
447579
+ await execSafe("ffmpeg", ["-y", "-i", videoPath, "-filter:v", `setpts=${slowFactor}*PTS`, "-an", outputPath]);
447580
+ } else if (ratio <= 1.4) {
447581
+ const slowFactor = (1 / ratio).toFixed(4);
447582
+ await execSafe("ffmpeg", ["-y", "-i", videoPath, "-filter:v", `minterpolate=fps=60:mi_mode=mci,setpts=${slowFactor}*PTS`, "-an", outputPath]);
447583
+ } else {
447584
+ const slowRatio = 0.7;
447585
+ const slowedDuration = videoDuration / slowRatio;
447586
+ const freezeDuration = targetDuration - slowedDuration;
447587
+ if (freezeDuration <= 0) {
447588
+ const slowFactor = (1 / ratio).toFixed(4);
447589
+ await execSafe("ffmpeg", ["-y", "-i", videoPath, "-filter:v", `minterpolate=fps=60:mi_mode=mci,setpts=${slowFactor}*PTS`, "-an", outputPath]);
447590
+ } else {
447591
+ const slowFactor = (1 / slowRatio).toFixed(4);
447592
+ await execSafe("ffmpeg", ["-y", "-i", videoPath, "-filter:v", `setpts=${slowFactor}*PTS,tpad=stop_mode=clone:stop_duration=${freezeDuration.toFixed(2)}`, "-an", outputPath]);
447593
+ }
447594
+ }
447595
+ }
447596
+ var init_audio = __esm({
447597
+ "../cli/src/utils/audio.ts"() {
447598
+ "use strict";
447599
+ init_exec_safe();
447600
+ }
447601
+ });
447602
+
447110
447603
  // ../cli/src/commands/_shared/hf-skill-bundle/bundle-content.ts
447111
447604
  var SKILL_MD, HOUSE_STYLE_MD, MOTION_PRINCIPLES_MD, TYPOGRAPHY_MD, TRANSITIONS_MD;
447112
447605
  var init_bundle_content = __esm({
@@ -448703,7 +449196,7 @@ async function getComposePrompts(opts) {
448703
449196
  return baseError(`DESIGN.md not found at ${designPath}. Run \`vibe scene init <dir>\` first.`);
448704
449197
  }
448705
449198
  if (!existsSync24(storyboardPath)) {
448706
- return baseError(`STORYBOARD.md not found at ${storyboardPath}. Run \`vibe scene init <dir>\` first.`);
449199
+ return baseError(`STORYBOARD.md not found at ${storyboardPath}. Run \`vibe scene init <dir>\` to create a starter, or add STORYBOARD.md with per-beat cues.`);
448707
449200
  }
448708
449201
  if (!existsSync24(skillPath)) {
448709
449202
  warnings.push(
@@ -448929,7 +449422,10 @@ async function executeSceneBuild(opts) {
448929
449422
  const mode = resolveSceneBuildMode(opts);
448930
449423
  const storyboardPath = join25(projectDir, "STORYBOARD.md");
448931
449424
  if (!existsSync26(storyboardPath)) {
448932
- return failBeforePrimitives(`STORYBOARD.md not found at ${storyboardPath}`, startedAt);
449425
+ return failBeforePrimitives(
449426
+ `STORYBOARD.md not found at ${storyboardPath}. Run \`vibe scene init <dir>\` to create a starter, or add STORYBOARD.md with per-beat cues.`,
449427
+ startedAt
449428
+ );
448933
449429
  }
448934
449430
  const storyboardMd = await readFile9(storyboardPath, "utf-8");
448935
449431
  const parsed = parseStoryboard(storyboardMd);
@@ -449009,6 +449505,13 @@ async function executeSceneBuild(opts) {
449009
449505
  }
449010
449506
  composeData = composeResult.data;
449011
449507
  }
449508
+ if (!existsSync26(join25(projectDir, "index.html"))) {
449509
+ await scaffoldSceneProject({
449510
+ dir: projectDir,
449511
+ name: projectDir.split(/[\\/]/).filter(Boolean).pop(),
449512
+ profile: "full"
449513
+ });
449514
+ }
449012
449515
  await syncRootClipReferences(parsed.beats, projectDir, beatOutcomes);
449013
449516
  let outputPath;
449014
449517
  let renderResult;
@@ -449107,6 +449610,7 @@ async function dispatchBackdrop(beat, ctx) {
449107
449610
  ctx.onProgress({ type: "backdrop-cached", beatId: beat.id, path: rel });
449108
449611
  return { status: "cached", path: rel };
449109
449612
  }
449613
+ loadSceneBuildEnv(ctx.projectDir);
449110
449614
  const apiKey = process.env.OPENAI_API_KEY ?? "";
449111
449615
  if (!apiKey) {
449112
449616
  const error = "OPENAI_API_KEY not set \u2014 cannot dispatch backdrop";
@@ -449135,6 +449639,18 @@ async function dispatchBackdrop(beat, ctx) {
449135
449639
  });
449136
449640
  return { status: "generated", path: rel };
449137
449641
  }
449642
+ function loadSceneBuildEnv(projectDir) {
449643
+ (0, import_dotenv2.config)({ path: join25(projectDir, ".env"), quiet: true });
449644
+ (0, import_dotenv2.config)({ path: resolve19(process.cwd(), ".env"), quiet: true });
449645
+ let dir = process.cwd();
449646
+ while (dir !== dirname15(dir)) {
449647
+ if (existsSync26(join25(dir, "pnpm-workspace.yaml"))) {
449648
+ (0, import_dotenv2.config)({ path: join25(dir, ".env"), quiet: true });
449649
+ return;
449650
+ }
449651
+ dir = dirname15(dir);
449652
+ }
449653
+ }
449138
449654
  async function skipped(kind, beatId, reason, ctx) {
449139
449655
  ctx.onProgress({ type: `${kind}-skipped`, beatId, reason });
449140
449656
  return { status: "skipped" };
@@ -449165,20 +449681,24 @@ async function syncRootClipReferences(beats, projectDir, outcomes) {
449165
449681
  const clipLines = [];
449166
449682
  const audioLines = [];
449167
449683
  for (const beat of beats) {
449168
- const duration = beat.duration ?? 3;
449684
+ const outcome = outcomes.find((o) => o.beatId === beat.id);
449685
+ const duration = await resolveBeatDuration({
449686
+ beatDuration: beat.duration,
449687
+ narrationPath: outcome?.narrationPath,
449688
+ projectDir
449689
+ });
449169
449690
  const compositionId = `scene-${beat.id}`;
449170
449691
  clipLines.push(
449171
449692
  ` <div class="clip" data-composition-id="${compositionId}" data-composition-src="compositions/${compositionId}.html" data-start="${cursor}" data-duration="${duration}" data-track-index="0"></div>`
449172
449693
  );
449173
- const outcome = outcomes.find((o) => o.beatId === beat.id);
449174
449694
  if (outcome?.narrationPath) {
449175
449695
  audioLines.push(
449176
- ` <audio src="${outcome.narrationPath}" data-start="${cursor}" data-duration="${duration}" data-track-index="2"></audio>`
449696
+ ` <audio id="narration-${beat.id}" src="${outcome.narrationPath}" data-start="${cursor}" data-duration="${duration}" data-track-index="2"></audio>`
449177
449697
  );
449178
449698
  }
449179
449699
  cursor += duration;
449180
449700
  }
449181
- const totalDuration = cursor;
449701
+ const totalDuration = Number(cursor.toFixed(2));
449182
449702
  const block = " <!-- vibe-scene-build: clip refs (auto-generated; safe to re-run) -->\n" + clipLines.join("\n") + (audioLines.length > 0 ? "\n" + audioLines.join("\n") : "") + "\n <!-- /vibe-scene-build -->";
449183
449703
  let next;
449184
449704
  const markerRe = /\n? *<!-- vibe-scene-build: clip refs.*?<!-- \/vibe-scene-build -->/s;
@@ -449196,22 +449716,36 @@ ${block}
449196
449716
  }
449197
449717
  }
449198
449718
  next = next.replace(
449199
- /(id="root"[\s\S]*?data-duration=")(\d+(?:\.\d+)?)(")/,
449719
+ /(id="root"[\s\S]*?data-duration=")([^"]*)(")/,
449200
449720
  `$1${totalDuration}$3`
449201
449721
  );
449202
449722
  if (next !== html) {
449203
449723
  await writeFile8(rootPath, next, "utf-8");
449204
449724
  }
449205
449725
  }
449726
+ async function resolveBeatDuration(opts) {
449727
+ const storyboardMin = opts.beatDuration ?? 3;
449728
+ if (!opts.narrationPath) return Number(storyboardMin.toFixed(2));
449729
+ try {
449730
+ const audioDuration = await getAudioDuration(join25(opts.projectDir, opts.narrationPath));
449731
+ return Number(Math.max(storyboardMin, audioDuration + 0.5).toFixed(2));
449732
+ } catch {
449733
+ return Number(storyboardMin.toFixed(2));
449734
+ }
449735
+ }
449736
+ var import_dotenv2;
449206
449737
  var init_scene_build = __esm({
449207
449738
  "../cli/src/commands/_shared/scene-build.ts"() {
449208
449739
  "use strict";
449740
+ import_dotenv2 = __toESM(require_main(), 1);
449209
449741
  init_dist();
449742
+ init_audio();
449210
449743
  init_compose_scenes_skills();
449211
449744
  init_compose_prompts();
449212
449745
  init_agent_host_detect();
449213
449746
  init_scene_render();
449214
449747
  init_storyboard_parse();
449748
+ init_scene_project();
449215
449749
  init_tts_resolve();
449216
449750
  }
449217
449751
  });
@@ -449232,6 +449766,7 @@ __export(output_exports, {
449232
449766
  notFoundError: () => notFoundError,
449233
449767
  outputError: () => outputError,
449234
449768
  outputResult: () => outputResult,
449769
+ outputSuccess: () => outputSuccess,
449235
449770
  spinner: () => spinner,
449236
449771
  suggestNext: () => suggestNext,
449237
449772
  usageError: () => usageError
@@ -449290,6 +449825,40 @@ function formatCost(min, max, unit) {
449290
449825
  if (min === max) return `~$${min.toFixed(2)} ${unit}`;
449291
449826
  return `~$${min.toFixed(2)}-$${max.toFixed(2)} ${unit}`;
449292
449827
  }
449828
+ function lookupCostEstimateUpperBound(command3) {
449829
+ return COST_ESTIMATES[command3]?.max ?? 0;
449830
+ }
449831
+ function outputSuccess(opts) {
449832
+ const elapsedMs = Math.max(0, Date.now() - opts.startedAt);
449833
+ const costUsd = opts.costUsd ?? (opts.dryRun ? lookupCostEstimateUpperBound(opts.command) : 0);
449834
+ const envelope = {
449835
+ command: opts.command,
449836
+ ...opts.dryRun ? { dryRun: true } : {},
449837
+ elapsedMs,
449838
+ costUsd,
449839
+ warnings: opts.warnings ?? [],
449840
+ data: opts.data
449841
+ };
449842
+ if (isJsonMode()) {
449843
+ const fields = process.env.VIBE_OUTPUT_FIELDS;
449844
+ if (fields) {
449845
+ const keys2 = fields.split(",").map((k) => k.trim());
449846
+ const data = opts.data;
449847
+ const filteredData = {};
449848
+ for (const key2 of keys2) {
449849
+ if (key2 in data) filteredData[key2] = data[key2];
449850
+ }
449851
+ envelope.data = filteredData;
449852
+ }
449853
+ console.log(JSON.stringify(envelope, null, 2));
449854
+ return;
449855
+ }
449856
+ if (isQuietMode()) {
449857
+ const data = opts.data;
449858
+ const primary = data.outputPath ?? data.output ?? data.path ?? data.url ?? data.id;
449859
+ if (primary !== void 0) console.log(String(primary));
449860
+ }
449861
+ }
449293
449862
  function outputResult(result) {
449294
449863
  if (result.dryRun && result.command && typeof result.command === "string") {
449295
449864
  const cost = COST_ESTIMATES[result.command];
@@ -449428,57 +449997,6 @@ var init_output = __esm({
449428
449997
  }
449429
449998
  });
449430
449999
 
449431
- // ../cli/src/utils/audio.ts
449432
- async function getAudioDuration(filePath) {
449433
- try {
449434
- return await ffprobeDuration(filePath);
449435
- } catch (error) {
449436
- const message = error instanceof Error ? error.message : String(error);
449437
- throw new Error(`Failed to get audio duration: ${message}`);
449438
- }
449439
- }
449440
- async function getVideoDuration(filePath) {
449441
- try {
449442
- return await ffprobeDuration(filePath);
449443
- } catch (error) {
449444
- const message = error instanceof Error ? error.message : String(error);
449445
- throw new Error(`Failed to get video duration: ${message}`);
449446
- }
449447
- }
449448
- async function extendVideoNaturally(videoPath, targetDuration, outputPath) {
449449
- const videoDuration = await getVideoDuration(videoPath);
449450
- const ratio = targetDuration / videoDuration;
449451
- if (ratio <= 1) {
449452
- const { copyFile: copyFile5 } = await import("node:fs/promises");
449453
- await copyFile5(videoPath, outputPath);
449454
- return;
449455
- }
449456
- if (ratio <= 1.15) {
449457
- const slowFactor = (1 / ratio).toFixed(4);
449458
- await execSafe("ffmpeg", ["-y", "-i", videoPath, "-filter:v", `setpts=${slowFactor}*PTS`, "-an", outputPath]);
449459
- } else if (ratio <= 1.4) {
449460
- const slowFactor = (1 / ratio).toFixed(4);
449461
- await execSafe("ffmpeg", ["-y", "-i", videoPath, "-filter:v", `minterpolate=fps=60:mi_mode=mci,setpts=${slowFactor}*PTS`, "-an", outputPath]);
449462
- } else {
449463
- const slowRatio = 0.7;
449464
- const slowedDuration = videoDuration / slowRatio;
449465
- const freezeDuration = targetDuration - slowedDuration;
449466
- if (freezeDuration <= 0) {
449467
- const slowFactor = (1 / ratio).toFixed(4);
449468
- await execSafe("ffmpeg", ["-y", "-i", videoPath, "-filter:v", `minterpolate=fps=60:mi_mode=mci,setpts=${slowFactor}*PTS`, "-an", outputPath]);
449469
- } else {
449470
- const slowFactor = (1 / slowRatio).toFixed(4);
449471
- await execSafe("ffmpeg", ["-y", "-i", videoPath, "-filter:v", `setpts=${slowFactor}*PTS,tpad=stop_mode=clone:stop_duration=${freezeDuration.toFixed(2)}`, "-an", outputPath]);
449472
- }
449473
- }
449474
- }
449475
- var init_audio = __esm({
449476
- "../cli/src/utils/audio.ts"() {
449477
- "use strict";
449478
- init_exec_safe();
449479
- }
449480
- });
449481
-
449482
450000
  // ../cli/src/utils/subtitle.ts
449483
450001
  function detectFormat(outputPath, explicitFormat) {
449484
450002
  if (explicitFormat) {
@@ -451881,6 +452399,7 @@ Examples:
451881
452399
  $ vibe ed sc video.mp4 --dry-run --json
451882
452400
 
451883
452401
  No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires GOOGLE_API_KEY).`).action(async (videoPath, options) => {
452402
+ const startedAt = Date.now();
451884
452403
  try {
451885
452404
  if (options.output) {
451886
452405
  validateOutputPath(options.output);
@@ -451897,16 +452416,19 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
451897
452416
  const outputPath = options.output || `${name}-cut${ext}`;
451898
452417
  const useGemini = options.useGemini || false;
451899
452418
  if (options.dryRun) {
451900
- outputResult({
451901
- dryRun: true,
452419
+ outputSuccess({
451902
452420
  command: "edit silence-cut",
451903
- params: {
451904
- videoPath: absVideoPath,
451905
- noiseThreshold: parseFloat(options.noise),
451906
- minDuration: parseFloat(options.minDuration),
451907
- padding: parseFloat(options.padding),
451908
- useGemini,
451909
- analyzeOnly: options.analyzeOnly || false
452421
+ startedAt,
452422
+ dryRun: true,
452423
+ data: {
452424
+ params: {
452425
+ videoPath: absVideoPath,
452426
+ noiseThreshold: parseFloat(options.noise),
452427
+ minDuration: parseFloat(options.minDuration),
452428
+ padding: parseFloat(options.padding),
452429
+ useGemini,
452430
+ analyzeOnly: options.analyzeOnly || false
452431
+ }
451910
452432
  }
451911
452433
  });
451912
452434
  return;
@@ -451931,13 +452453,16 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
451931
452453
  }
451932
452454
  spinner2.succeed(source_default.green("Silence detection complete"));
451933
452455
  if (isJsonMode()) {
451934
- outputResult({
451935
- success: true,
451936
- method: result.method,
451937
- totalDuration: result.totalDuration,
451938
- silentPeriods: result.silentPeriods,
451939
- silentDuration: result.silentDuration,
451940
- outputPath: result.outputPath
452456
+ outputSuccess({
452457
+ command: "edit silence-cut",
452458
+ startedAt,
452459
+ data: {
452460
+ method: result.method,
452461
+ totalDuration: result.totalDuration,
452462
+ silentPeriods: result.silentPeriods,
452463
+ silentDuration: result.silentDuration,
452464
+ outputPath: result.outputPath
452465
+ }
451941
452466
  });
451942
452467
  return;
451943
452468
  }
@@ -451975,6 +452500,7 @@ Examples:
451975
452500
  $ vibe ed cap video.mp4 --dry-run --json
451976
452501
 
451977
452502
  Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoPath, options) => {
452503
+ const startedAt = Date.now();
451978
452504
  try {
451979
452505
  if (options.output) {
451980
452506
  validateOutputPath(options.output);
@@ -451987,16 +452513,19 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
451987
452513
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
451988
452514
  }
451989
452515
  if (options.dryRun) {
451990
- outputResult({
451991
- dryRun: true,
452516
+ outputSuccess({
451992
452517
  command: "edit caption",
451993
- params: {
451994
- videoPath: absVideoPath,
451995
- style: options.style,
451996
- fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
451997
- fontColor: options.color,
451998
- language: options.language,
451999
- position: options.position
452518
+ startedAt,
452519
+ dryRun: true,
452520
+ data: {
452521
+ params: {
452522
+ videoPath: absVideoPath,
452523
+ style: options.style,
452524
+ fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
452525
+ fontColor: options.color,
452526
+ language: options.language,
452527
+ position: options.position
452528
+ }
452000
452529
  }
452001
452530
  });
452002
452531
  return;
@@ -452025,12 +452554,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452025
452554
  }
452026
452555
  spinner2.succeed(source_default.green("Captions applied"));
452027
452556
  if (isJsonMode()) {
452028
- outputResult({
452029
- success: true,
452030
- segmentCount: result.segmentCount,
452031
- style: options.style || "bold",
452032
- outputPath: result.outputPath,
452033
- srtPath: result.srtPath
452557
+ outputSuccess({
452558
+ command: "edit caption",
452559
+ startedAt,
452560
+ data: {
452561
+ segmentCount: result.segmentCount,
452562
+ style: options.style || "bold",
452563
+ outputPath: result.outputPath,
452564
+ srtPath: result.srtPath
452565
+ }
452034
452566
  });
452035
452567
  return;
452036
452568
  }
@@ -452049,6 +452581,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452049
452581
  }
452050
452582
  });
452051
452583
  aiCommand.command("noise-reduce").description("Remove background noise from audio/video using FFmpeg (no API key needed)").argument("<input>", "Audio or video file path").option("-o, --output <path>", "Output file path (default: <name>-denoised.<ext>)").option("-s, --strength <level>", "Noise reduction strength: low, medium, high (default: medium)", "medium").option("-n, --noise-floor <dB>", "Custom noise floor in dB (overrides strength preset)").option("--dry-run", "Preview parameters without executing").action(async (inputPath, options) => {
452584
+ const startedAt = Date.now();
452052
452585
  try {
452053
452586
  if (options.output) {
452054
452587
  validateOutputPath(options.output);
@@ -452061,13 +452594,16 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452061
452594
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
452062
452595
  }
452063
452596
  if (options.dryRun) {
452064
- outputResult({
452065
- dryRun: true,
452597
+ outputSuccess({
452066
452598
  command: "edit noise-reduce",
452067
- params: {
452068
- inputPath: absInputPath,
452069
- strength: options.strength,
452070
- noiseFloor: options.noiseFloor ? parseFloat(options.noiseFloor) : void 0
452599
+ startedAt,
452600
+ dryRun: true,
452601
+ data: {
452602
+ params: {
452603
+ inputPath: absInputPath,
452604
+ strength: options.strength,
452605
+ noiseFloor: options.noiseFloor ? parseFloat(options.noiseFloor) : void 0
452606
+ }
452071
452607
  }
452072
452608
  });
452073
452609
  return;
@@ -452088,11 +452624,14 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452088
452624
  }
452089
452625
  spinner2.succeed(source_default.green("Noise reduction complete"));
452090
452626
  if (isJsonMode()) {
452091
- outputResult({
452092
- success: true,
452093
- inputDuration: result.inputDuration,
452094
- strength: options.strength || "medium",
452095
- outputPath: result.outputPath
452627
+ outputSuccess({
452628
+ command: "edit noise-reduce",
452629
+ startedAt,
452630
+ data: {
452631
+ inputDuration: result.inputDuration,
452632
+ strength: options.strength || "medium",
452633
+ outputPath: result.outputPath
452634
+ }
452096
452635
  });
452097
452636
  return;
452098
452637
  }
@@ -452108,6 +452647,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452108
452647
  }
452109
452648
  });
452110
452649
  aiCommand.command("fade").description("Apply fade in/out effects to video (FFmpeg only, no API key needed)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-faded.<ext>)").option("--fade-in <seconds>", "Fade-in duration in seconds (default: 1)", "1").option("--fade-out <seconds>", "Fade-out duration in seconds (default: 1)", "1").option("--audio-only", "Apply fade to audio only (video stream copied)").option("--video-only", "Apply fade to video only (audio stream copied)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
452650
+ const startedAt = Date.now();
452111
452651
  try {
452112
452652
  if (options.output) {
452113
452653
  validateOutputPath(options.output);
@@ -452120,15 +452660,18 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452120
452660
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
452121
452661
  }
452122
452662
  if (options.dryRun) {
452123
- outputResult({
452124
- dryRun: true,
452663
+ outputSuccess({
452125
452664
  command: "edit fade",
452126
- params: {
452127
- videoPath: absVideoPath,
452128
- fadeIn: parseFloat(options.fadeIn),
452129
- fadeOut: parseFloat(options.fadeOut),
452130
- audioOnly: options.audioOnly || false,
452131
- videoOnly: options.videoOnly || false
452665
+ startedAt,
452666
+ dryRun: true,
452667
+ data: {
452668
+ params: {
452669
+ videoPath: absVideoPath,
452670
+ fadeIn: parseFloat(options.fadeIn),
452671
+ fadeOut: parseFloat(options.fadeOut),
452672
+ audioOnly: options.audioOnly || false,
452673
+ videoOnly: options.videoOnly || false
452674
+ }
452132
452675
  }
452133
452676
  });
452134
452677
  return;
@@ -452151,12 +452694,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452151
452694
  }
452152
452695
  spinner2.succeed(source_default.green("Fade effects applied"));
452153
452696
  if (isJsonMode()) {
452154
- outputResult({
452155
- success: true,
452156
- totalDuration: result.totalDuration,
452157
- fadeInApplied: result.fadeInApplied,
452158
- fadeOutApplied: result.fadeOutApplied,
452159
- outputPath: result.outputPath
452697
+ outputSuccess({
452698
+ command: "edit fade",
452699
+ startedAt,
452700
+ data: {
452701
+ totalDuration: result.totalDuration,
452702
+ fadeInApplied: result.fadeInApplied,
452703
+ fadeOutApplied: result.fadeOutApplied,
452704
+ outputPath: result.outputPath
452705
+ }
452160
452706
  });
452161
452707
  return;
452162
452708
  }
@@ -452173,6 +452719,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452173
452719
  }
452174
452720
  });
452175
452721
  aiCommand.command("translate-srt").description("Translate SRT subtitle file to another language (Claude/OpenAI)").argument("<srt>", "SRT file path").option("-t, --target <language>", "Target language (e.g., ko, es, fr, ja, zh)").option("-o, --output <path>", "Output file path (default: <name>-<target>.srt)").option("-p, --provider <provider>", "Translation provider: claude, openai (default: claude)", "claude").option("--source <language>", "Source language (auto-detected if omitted)").option("-k, --api-key <key>", "API key (or set ANTHROPIC_API_KEY / OPENAI_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (srtPath, options) => {
452722
+ const startedAt = Date.now();
452176
452723
  try {
452177
452724
  if (options.output) {
452178
452725
  validateOutputPath(options.output);
@@ -452185,14 +452732,17 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452185
452732
  exitWithError(notFoundError(absSrtPath));
452186
452733
  }
452187
452734
  if (options.dryRun) {
452188
- outputResult({
452189
- dryRun: true,
452735
+ outputSuccess({
452190
452736
  command: "edit translate-srt",
452191
- params: {
452192
- srtPath: absSrtPath,
452193
- targetLanguage: options.target,
452194
- provider: options.provider || "claude",
452195
- sourceLanguage: options.source
452737
+ startedAt,
452738
+ dryRun: true,
452739
+ data: {
452740
+ params: {
452741
+ srtPath: absSrtPath,
452742
+ targetLanguage: options.target,
452743
+ provider: options.provider || "claude",
452744
+ sourceLanguage: options.source
452745
+ }
452196
452746
  }
452197
452747
  });
452198
452748
  return;
@@ -452222,12 +452772,15 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452222
452772
  }
452223
452773
  spinner2.succeed(source_default.green("Translation complete"));
452224
452774
  if (isJsonMode()) {
452225
- outputResult({
452226
- success: true,
452227
- segmentCount: result.segmentCount,
452228
- sourceLanguage: result.sourceLanguage,
452229
- targetLanguage: result.targetLanguage,
452230
- outputPath: result.outputPath
452775
+ outputSuccess({
452776
+ command: "edit translate-srt",
452777
+ startedAt,
452778
+ data: {
452779
+ segmentCount: result.segmentCount,
452780
+ sourceLanguage: result.sourceLanguage,
452781
+ targetLanguage: result.targetLanguage,
452782
+ outputPath: result.outputPath
452783
+ }
452231
452784
  });
452232
452785
  return;
452233
452786
  }
@@ -452244,6 +452797,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452244
452797
  }
452245
452798
  });
452246
452799
  aiCommand.command("jump-cut").description("Remove filler words (um, uh, like, etc.) from video using Whisper word-level timestamps").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-jumpcut.<ext>)").option("--fillers <words>", "Comma-separated filler words to detect").option("--padding <seconds>", "Padding around cuts in seconds (default: 0.05)", "0.05").option("-l, --language <lang>", "Language code for transcription (e.g., en, ko)").option("--analyze-only", "Only detect fillers, don't cut").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
452800
+ const startedAt = Date.now();
452247
452801
  try {
452248
452802
  if (options.output) {
452249
452803
  validateOutputPath(options.output);
@@ -452258,15 +452812,18 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452258
452812
  }
452259
452813
  if (options.dryRun) {
452260
452814
  const fillers2 = options.fillers ? options.fillers.split(",").map((f) => f.trim()) : void 0;
452261
- outputResult({
452262
- dryRun: true,
452815
+ outputSuccess({
452263
452816
  command: "edit jump-cut",
452264
- params: {
452265
- videoPath: absVideoPath,
452266
- fillers: fillers2,
452267
- padding: parseFloat(options.padding),
452268
- language: options.language,
452269
- analyzeOnly: options.analyzeOnly || false
452817
+ startedAt,
452818
+ dryRun: true,
452819
+ data: {
452820
+ params: {
452821
+ videoPath: absVideoPath,
452822
+ fillers: fillers2,
452823
+ padding: parseFloat(options.padding),
452824
+ language: options.language,
452825
+ analyzeOnly: options.analyzeOnly || false
452826
+ }
452270
452827
  }
452271
452828
  });
452272
452829
  return;
@@ -452295,13 +452852,16 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452295
452852
  }
452296
452853
  spinner2.succeed(source_default.green("Filler detection complete"));
452297
452854
  if (isJsonMode()) {
452298
- outputResult({
452299
- success: true,
452300
- totalDuration: result.totalDuration,
452301
- fillerCount: result.fillerCount,
452302
- fillerDuration: result.fillerDuration,
452303
- fillers: result.fillers,
452304
- outputPath: result.outputPath
452855
+ outputSuccess({
452856
+ command: "edit jump-cut",
452857
+ startedAt,
452858
+ data: {
452859
+ totalDuration: result.totalDuration,
452860
+ fillerCount: result.fillerCount,
452861
+ fillerDuration: result.fillerDuration,
452862
+ fillers: result.fillers,
452863
+ outputPath: result.outputPath
452864
+ }
452305
452865
  });
452306
452866
  return;
452307
452867
  }
@@ -453446,6 +454006,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453446
454006
  registerEditCommands(editCommand);
453447
454007
  registerFillGapsCommand(editCommand);
453448
454008
  editCommand.command("grade").description("Apply AI-generated color grading (Claude + FFmpeg)").argument("<video>", "Video file path").option("-s, --style <prompt>", "Style description (e.g., 'cinematic warm')").option("--preset <name>", "Built-in preset: film-noir, vintage, cinematic-warm, cool-tones, high-contrast, pastel, cyberpunk, horror").option("-o, --output <path>", "Output video file path").option("--analyze-only", "Show filter without applying").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
454009
+ const startedAt = Date.now();
453449
454010
  try {
453450
454011
  if (options.style) rejectControlChars(options.style);
453451
454012
  if (options.output) {
@@ -453461,13 +454022,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453461
454022
  exitWithError(notFoundError("FFmpeg not found. Install with: brew install ffmpeg"));
453462
454023
  }
453463
454024
  if (options.dryRun) {
453464
- outputResult({
453465
- dryRun: true,
454025
+ outputSuccess({
453466
454026
  command: "edit grade",
453467
- params: {
453468
- videoPath: resolve29(process.cwd(), videoPath),
453469
- style: options.style || options.preset,
453470
- analyzeOnly: options.analyzeOnly || false
454027
+ startedAt,
454028
+ dryRun: true,
454029
+ data: {
454030
+ params: {
454031
+ videoPath: resolve29(process.cwd(), videoPath),
454032
+ style: options.style || options.preset,
454033
+ analyzeOnly: options.analyzeOnly || false
454034
+ }
453471
454035
  }
453472
454036
  });
453473
454037
  return;
@@ -453493,12 +454057,15 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453493
454057
  if (isJsonMode()) {
453494
454058
  const absPath2 = resolve29(process.cwd(), videoPath);
453495
454059
  const gradeOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath2.replace(/(\.[^.]+)$/, "-graded$1");
453496
- outputResult({
453497
- success: true,
453498
- style: options.preset || options.style,
453499
- description: gradeResult.description,
453500
- ffmpegFilter: gradeResult.ffmpegFilter,
453501
- outputPath: options.analyzeOnly ? void 0 : gradeOutputPath
454060
+ outputSuccess({
454061
+ command: "edit grade",
454062
+ startedAt,
454063
+ data: {
454064
+ style: options.preset || options.style,
454065
+ description: gradeResult.description,
454066
+ ffmpegFilter: gradeResult.ffmpegFilter,
454067
+ outputPath: options.analyzeOnly ? void 0 : gradeOutputPath
454068
+ }
453502
454069
  });
453503
454070
  return;
453504
454071
  }
@@ -453527,6 +454094,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453527
454094
  }
453528
454095
  });
453529
454096
  editCommand.command("text-overlay").description("Apply text overlays to video (FFmpeg drawtext)").argument("<video>", "Video file path").option("-t, --text <texts...>", "Text lines to overlay (repeat for multiple)").option("-s, --style <style>", "Overlay style: lower-third, center-bold, subtitle, minimal", "lower-third").option("--font-size <size>", "Font size in pixels (auto-calculated if omitted)").option("--font-color <color>", "Font color (default: white)", "white").option("--fade <seconds>", "Fade in/out duration in seconds", "0.3").option("--start <seconds>", "Start time in seconds", "0").option("--end <seconds>", "End time in seconds (default: video duration)").option("-o, --output <path>", "Output video file path").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
454097
+ const startedAt = Date.now();
453530
454098
  try {
453531
454099
  if (!options.text || options.text.length === 0) {
453532
454100
  exitWithError(usageError("At least one --text option is required", 'Example: vibe edit text-overlay video.mp4 -t "NEXUS AI" --style center-bold'));
@@ -453539,18 +454107,21 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453539
454107
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
453540
454108
  }
453541
454109
  if (options.dryRun) {
453542
- outputResult({
453543
- dryRun: true,
454110
+ outputSuccess({
453544
454111
  command: "edit text-overlay",
453545
- params: {
453546
- videoPath: resolve29(process.cwd(), videoPath),
453547
- texts: options.text,
453548
- style: options.style,
453549
- fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
453550
- fontColor: options.fontColor,
453551
- fade: parseFloat(options.fade),
453552
- start: parseFloat(options.start),
453553
- end: options.end ? parseFloat(options.end) : void 0
454112
+ startedAt,
454113
+ dryRun: true,
454114
+ data: {
454115
+ params: {
454116
+ videoPath: resolve29(process.cwd(), videoPath),
454117
+ texts: options.text,
454118
+ style: options.style,
454119
+ fontSize: options.fontSize ? parseInt(options.fontSize) : void 0,
454120
+ fontColor: options.fontColor,
454121
+ fade: parseFloat(options.fade),
454122
+ start: parseFloat(options.start),
454123
+ end: options.end ? parseFloat(options.end) : void 0
454124
+ }
453554
454125
  }
453555
454126
  });
453556
454127
  return;
@@ -453575,11 +454146,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453575
454146
  }
453576
454147
  spinner2.succeed(source_default.green("Text overlays applied"));
453577
454148
  if (isJsonMode()) {
453578
- outputResult({
453579
- success: true,
453580
- style: options.style,
453581
- texts: options.text,
453582
- outputPath: result.outputPath
454149
+ outputSuccess({
454150
+ command: "edit text-overlay",
454151
+ startedAt,
454152
+ data: {
454153
+ style: options.style,
454154
+ texts: options.text,
454155
+ outputPath: result.outputPath
454156
+ }
453583
454157
  });
453584
454158
  return;
453585
454159
  }
@@ -453596,6 +454170,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453596
454170
  }
453597
454171
  });
453598
454172
  editCommand.command("speed-ramp").description("Apply content-aware speed ramping (Whisper + Claude + FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output video file path").option("-s, --style <style>", "Style: dramatic, smooth, action", "dramatic").option("--min-speed <factor>", "Minimum speed factor", "0.25").option("--max-speed <factor>", "Maximum speed factor", "4.0").option("--analyze-only", "Show keyframes without applying").option("-l, --language <lang>", "Language code for transcription").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
454173
+ const startedAt = Date.now();
453599
454174
  try {
453600
454175
  if (options.output) {
453601
454176
  validateOutputPath(options.output);
@@ -453604,15 +454179,18 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453604
454179
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
453605
454180
  }
453606
454181
  if (options.dryRun) {
453607
- outputResult({
453608
- dryRun: true,
454182
+ outputSuccess({
453609
454183
  command: "edit speed-ramp",
453610
- params: {
453611
- videoPath: resolve29(process.cwd(), videoPath),
453612
- style: options.style,
453613
- minSpeed: parseFloat(options.minSpeed),
453614
- maxSpeed: parseFloat(options.maxSpeed),
453615
- analyzeOnly: options.analyzeOnly || false
454184
+ startedAt,
454185
+ dryRun: true,
454186
+ data: {
454187
+ params: {
454188
+ videoPath: resolve29(process.cwd(), videoPath),
454189
+ style: options.style,
454190
+ minSpeed: parseFloat(options.minSpeed),
454191
+ maxSpeed: parseFloat(options.maxSpeed),
454192
+ analyzeOnly: options.analyzeOnly || false
454193
+ }
453616
454194
  }
453617
454195
  });
453618
454196
  return;
@@ -453665,11 +454243,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453665
454243
  if (isJsonMode()) {
453666
454244
  const avgSpeed2 = speedResult.keyframes.reduce((sum, kf) => sum + kf.speed, 0) / speedResult.keyframes.length;
453667
454245
  const speedRampOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, "-ramped$1");
453668
- outputResult({
453669
- success: true,
453670
- keyframes: speedResult.keyframes,
453671
- avgSpeed: avgSpeed2,
453672
- outputPath: options.analyzeOnly ? void 0 : speedRampOutputPath
454246
+ outputSuccess({
454247
+ command: "edit speed-ramp",
454248
+ startedAt,
454249
+ data: {
454250
+ keyframes: speedResult.keyframes,
454251
+ avgSpeed: avgSpeed2,
454252
+ outputPath: options.analyzeOnly ? void 0 : speedRampOutputPath
454253
+ }
453673
454254
  });
453674
454255
  return;
453675
454256
  }
@@ -453709,6 +454290,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453709
454290
  }
453710
454291
  });
453711
454292
  editCommand.command("reframe").description("Auto-reframe video to different aspect ratio (Claude Vision + FFmpeg)").argument("<video>", "Video file path").option("-a, --aspect <ratio>", "Target aspect ratio: 9:16, 1:1, 4:5", "9:16").option("-f, --focus <mode>", "Focus mode: auto, face, center, action", "auto").option("-o, --output <path>", "Output video file path").option("--analyze-only", "Show crop regions without applying").option("--keyframes <path>", "Export keyframes to JSON file").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
454293
+ const startedAt = Date.now();
453712
454294
  try {
453713
454295
  if (options.output) {
453714
454296
  validateOutputPath(options.output);
@@ -453717,14 +454299,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453717
454299
  exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
453718
454300
  }
453719
454301
  if (options.dryRun) {
453720
- outputResult({
453721
- dryRun: true,
454302
+ outputSuccess({
453722
454303
  command: "edit reframe",
453723
- params: {
453724
- videoPath: resolve29(process.cwd(), videoPath),
453725
- aspect: options.aspect,
453726
- focus: options.focus,
453727
- analyzeOnly: options.analyzeOnly || false
454304
+ startedAt,
454305
+ dryRun: true,
454306
+ data: {
454307
+ params: {
454308
+ videoPath: resolve29(process.cwd(), videoPath),
454309
+ aspect: options.aspect,
454310
+ focus: options.focus,
454311
+ analyzeOnly: options.analyzeOnly || false
454312
+ }
453728
454313
  }
453729
454314
  });
453730
454315
  return;
@@ -453792,13 +454377,16 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453792
454377
  spinner2.succeed(source_default.green(`Analyzed ${cropKeyframes.length} keyframes`));
453793
454378
  if (isJsonMode()) {
453794
454379
  const reframeOutputPath = options.output ? resolve29(process.cwd(), options.output) : absPath.replace(/(\.[^.]+)$/, `-${options.aspect.replace(":", "x")}$1`);
453795
- outputResult({
453796
- success: true,
453797
- sourceWidth,
453798
- sourceHeight,
453799
- aspect: options.aspect,
453800
- cropKeyframes,
453801
- outputPath: options.analyzeOnly ? void 0 : reframeOutputPath
454380
+ outputSuccess({
454381
+ command: "edit reframe",
454382
+ startedAt,
454383
+ data: {
454384
+ sourceWidth,
454385
+ sourceHeight,
454386
+ aspect: options.aspect,
454387
+ cropKeyframes,
454388
+ outputPath: options.analyzeOnly ? void 0 : reframeOutputPath
454389
+ }
453802
454390
  });
453803
454391
  return;
453804
454392
  }
@@ -453849,6 +454437,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453849
454437
  }
453850
454438
  });
453851
454439
  editCommand.command("image").description("Edit image(s) using AI (Gemini/OpenAI/Grok)").argument("<images...>", "Input image file(s) followed by edit prompt").option("-p, --provider <provider>", "Provider: gemini (default), openai, grok", "gemini").option("-k, --api-key <key>", "API key (or set env variable)").option("-o, --output <path>", "Output file path", "edited.png").option("-m, --model <model>", "Model: flash/3.1-flash/latest/pro (Gemini only)", "flash").option("-r, --ratio <ratio>", "Output aspect ratio").option("-s, --size <resolution>", "Resolution: 1K, 2K, 4K (Gemini Pro only)").option("--dry-run", "Preview parameters without executing").action(async (args, options) => {
454440
+ const startedAt = Date.now();
453852
454441
  try {
453853
454442
  if (args.length < 2) {
453854
454443
  exitWithError(usageError("Need at least one image and a prompt"));
@@ -453864,16 +454453,19 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453864
454453
  exitWithError(usageError("Grok supports only 1 input image for editing.", "Use -p gemini (up to 14 images) or -p openai (up to 16 images) for multi-image editing."));
453865
454454
  }
453866
454455
  if (options.dryRun) {
453867
- outputResult({
453868
- dryRun: true,
454456
+ outputSuccess({
453869
454457
  command: "edit image",
453870
- params: {
453871
- imagePaths: imagePaths.map((p) => resolve29(process.cwd(), p)),
453872
- prompt: prompt3,
453873
- provider,
453874
- model: options.model,
453875
- ratio: options.ratio,
453876
- size: options.size
454458
+ startedAt,
454459
+ dryRun: true,
454460
+ data: {
454461
+ params: {
454462
+ imagePaths: imagePaths.map((p) => resolve29(process.cwd(), p)),
454463
+ prompt: prompt3,
454464
+ provider,
454465
+ model: options.model,
454466
+ ratio: options.ratio,
454467
+ size: options.size
454468
+ }
453877
454469
  }
453878
454470
  });
453879
454471
  return;
@@ -453951,11 +454543,14 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453951
454543
  };
453952
454544
  const resultModel = result.model;
453953
454545
  if (isJsonMode()) {
453954
- outputResult({
453955
- success: true,
453956
- provider,
453957
- model: resultModel || options.model,
453958
- outputPath
454546
+ outputSuccess({
454547
+ command: "edit image",
454548
+ startedAt,
454549
+ data: {
454550
+ provider,
454551
+ model: resultModel || options.model,
454552
+ outputPath
454553
+ }
453959
454554
  });
453960
454555
  await saveImage();
453961
454556
  return;
@@ -453971,6 +454566,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453971
454566
  }
453972
454567
  });
453973
454568
  editCommand.command("interpolate").description("Create slow motion with frame interpolation (FFmpeg)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path").option("-f, --factor <number>", "Slow motion factor: 2, 4, or 8", "2").option("--fps <number>", "Target output FPS").option("--quality <mode>", "Quality: fast or quality", "quality").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
454569
+ const startedAt = Date.now();
453974
454570
  try {
453975
454571
  if (options.output) {
453976
454572
  validateOutputPath(options.output);
@@ -453981,14 +454577,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
453981
454577
  exitWithError(usageError("Factor must be 2, 4, or 8"));
453982
454578
  }
453983
454579
  if (options.dryRun) {
453984
- outputResult({
453985
- dryRun: true,
454580
+ outputSuccess({
453986
454581
  command: "edit interpolate",
453987
- params: {
453988
- videoPath: absPath,
453989
- factor,
453990
- fps: options.fps ? parseInt(options.fps) : void 0,
453991
- quality: options.quality
454582
+ startedAt,
454583
+ dryRun: true,
454584
+ data: {
454585
+ params: {
454586
+ videoPath: absPath,
454587
+ factor,
454588
+ fps: options.fps ? parseInt(options.fps) : void 0,
454589
+ quality: options.quality
454590
+ }
453992
454591
  }
453993
454592
  });
453994
454593
  return;
@@ -454015,12 +454614,15 @@ Run 'vibe schema edit.<command>' for structured parameter info.
454015
454614
  await execSafe("ffmpeg", ["-i", absPath, "-filter:v", `minterpolate='${mi}:fps=${targetFps}',setpts=${factor}*PTS`, "-an", outputPath, "-y"], { timeout: 6e5 });
454016
454615
  spinner2.succeed(source_default.green(`Created ${factor}x slow motion`));
454017
454616
  if (isJsonMode()) {
454018
- outputResult({
454019
- success: true,
454020
- originalFps,
454021
- targetFps,
454022
- factor,
454023
- outputPath
454617
+ outputSuccess({
454618
+ command: "edit interpolate",
454619
+ startedAt,
454620
+ data: {
454621
+ originalFps,
454622
+ targetFps,
454623
+ factor,
454624
+ outputPath
454625
+ }
454024
454626
  });
454025
454627
  return;
454026
454628
  }
@@ -454046,6 +454648,7 @@ Run 'vibe schema edit.<command>' for structured parameter info.
454046
454648
  }
454047
454649
  });
454048
454650
  editCommand.command("upscale-video").description("Upscale video resolution using AI or FFmpeg").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path").option("-s, --scale <factor>", "Scale factor: 2 or 4", "2").option("-m, --model <model>", "Model: real-esrgan, topaz", "real-esrgan").option("--ffmpeg", "Use FFmpeg lanczos (free, no API)").option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)").option("--no-wait", "Start processing and return task ID without waiting").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
454651
+ const startedAt = Date.now();
454049
454652
  try {
454050
454653
  if (options.output) {
454051
454654
  validateOutputPath(options.output);
@@ -454056,14 +454659,17 @@ Run 'vibe schema edit.<command>' for structured parameter info.
454056
454659
  exitWithError(usageError("Scale must be 2 or 4"));
454057
454660
  }
454058
454661
  if (options.dryRun) {
454059
- outputResult({
454060
- dryRun: true,
454662
+ outputSuccess({
454061
454663
  command: "edit upscale-video",
454062
- params: {
454063
- videoPath: absPath,
454064
- scale,
454065
- model: options.model,
454066
- ffmpeg: options.ffmpeg || false
454664
+ startedAt,
454665
+ dryRun: true,
454666
+ data: {
454667
+ params: {
454668
+ videoPath: absPath,
454669
+ scale,
454670
+ model: options.model,
454671
+ ffmpeg: options.ffmpeg || false
454672
+ }
454067
454673
  }
454068
454674
  });
454069
454675
  return;
@@ -454089,10 +454695,13 @@ Run 'vibe schema edit.<command>' for structured parameter info.
454089
454695
  await execSafe("ffmpeg", ["-i", absPath, "-vf", `scale=${newWidth}:${newHeight}:flags=lanczos`, "-c:a", "copy", outputPath, "-y"]);
454090
454696
  spinner3.succeed(source_default.green(`Upscaled to ${newWidth}x${newHeight}`));
454091
454697
  if (isJsonMode()) {
454092
- outputResult({
454093
- success: true,
454094
- dimensions: `${newWidth}x${newHeight}`,
454095
- outputPath
454698
+ outputSuccess({
454699
+ command: "edit upscale-video",
454700
+ startedAt,
454701
+ data: {
454702
+ dimensions: `${newWidth}x${newHeight}`,
454703
+ outputPath
454704
+ }
454096
454705
  });
454097
454706
  return;
454098
454707
  }
@@ -454797,21 +455406,25 @@ Score each category 1-10. For fixable issues, provide an FFmpeg filter in autoFi
454797
455406
  }
454798
455407
  function registerReviewCommand(aiCommand) {
454799
455408
  aiCommand.command("review").description("Review video quality using Gemini AI and optionally auto-fix issues").argument("<video>", "Video file path").option("-s, --storyboard <path>", "Storyboard JSON file for context").option("--auto-apply", "Automatically apply fixable corrections").option("--verify", "Run verification pass after applying fixes").option("-m, --model <model>", "Gemini model: flash (default), flash-2.5, pro", "flash").option("-o, --output <path>", "Output video file path (for auto-apply)").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
455409
+ const startedAt = Date.now();
454800
455410
  try {
454801
455411
  if (options.output) {
454802
455412
  validateOutputPath(options.output);
454803
455413
  }
454804
455414
  if (options.dryRun) {
454805
- outputResult({
454806
- dryRun: true,
455415
+ outputSuccess({
454807
455416
  command: "ai review",
454808
- params: {
454809
- videoPath,
454810
- storyboard: options.storyboard,
454811
- autoApply: options.autoApply ?? false,
454812
- verify: options.verify ?? false,
454813
- model: options.model,
454814
- output: options.output
455417
+ startedAt,
455418
+ dryRun: true,
455419
+ data: {
455420
+ params: {
455421
+ videoPath,
455422
+ storyboard: options.storyboard,
455423
+ autoApply: options.autoApply ?? false,
455424
+ verify: options.verify ?? false,
455425
+ model: options.model,
455426
+ output: options.output
455427
+ }
454815
455428
  }
454816
455429
  });
454817
455430
  return;
@@ -455079,27 +455692,31 @@ Use this image analysis to inform the color palette, typography placement, and o
455079
455692
  }
455080
455693
  function registerMotionCommand(aiCommand) {
455081
455694
  aiCommand.command("motion").description("Generate motion graphics using Claude + Remotion (render & composite)").argument("<description>", "Natural language description of the motion graphic").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("-o, --output <path>", "Output file path", "motion.tsx").option("-d, --duration <sec>", "Duration in seconds", "5").option("-w, --width <px>", "Width in pixels", "1920").option("-h, --height <px>", "Height in pixels", "1080").option("--fps <fps>", "Frame rate", "30").option("-s, --style <style>", "Style preset: minimal, corporate, playful, cinematic").option("--render", "Render the generated code with Remotion (output .webm)").option("--video <path>", "Base video to composite the motion graphic onto").option("--image <path>", "Image to analyze with Gemini \u2014 color/mood fed into Claude prompt").option("--from-tsx <path>", "Refine an existing TSX file instead of generating from scratch").option("-m, --model <alias>", "LLM model: sonnet (default), opus, gemini, gemini-3.1-pro", "sonnet").option("--dry-run", "Preview parameters without executing").action(async (description, options) => {
455695
+ const startedAt = Date.now();
455082
455696
  try {
455083
455697
  if (options.output) {
455084
455698
  validateOutputPath(options.output);
455085
455699
  }
455086
455700
  if (options.dryRun) {
455087
- outputResult({
455701
+ outputSuccess({
455702
+ command: "generate motion",
455703
+ startedAt,
455088
455704
  dryRun: true,
455089
- command: "ai motion",
455090
- params: {
455091
- description: description.slice(0, 200),
455092
- duration: options.duration,
455093
- width: options.width,
455094
- height: options.height,
455095
- fps: options.fps,
455096
- style: options.style,
455097
- render: options.render ?? false,
455098
- video: options.video,
455099
- image: options.image,
455100
- fromTsx: options.fromTsx,
455101
- model: options.model,
455102
- output: options.output
455705
+ data: {
455706
+ params: {
455707
+ description: description.slice(0, 200),
455708
+ duration: options.duration,
455709
+ width: options.width,
455710
+ height: options.height,
455711
+ fps: options.fps,
455712
+ style: options.style,
455713
+ render: options.render ?? false,
455714
+ video: options.video,
455715
+ image: options.image,
455716
+ fromTsx: options.fromTsx,
455717
+ model: options.model,
455718
+ output: options.output
455719
+ }
455103
455720
  }
455104
455721
  });
455105
455722
  return;
@@ -455213,20 +455830,24 @@ async function executeSoundEffect(options) {
455213
455830
  }
455214
455831
  function registerSoundEffectCommand(parent) {
455215
455832
  parent.command("sound-effect").description("Generate sound effect using ElevenLabs").argument("<prompt>", "Description of the sound effect").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-o, --output <path>", "Output audio file path", "sound-effect.mp3").option("-d, --duration <seconds>", "Duration in seconds (0.5-22, default: auto)").option("--prompt-influence <value>", "Prompt influence (0-1, default: 0.3)").option("--dry-run", "Preview parameters without executing").action(async (prompt3, options) => {
455833
+ const startedAt = Date.now();
455216
455834
  try {
455217
455835
  rejectControlChars(prompt3);
455218
455836
  if (options.output) {
455219
455837
  validateOutputPath(options.output);
455220
455838
  }
455221
455839
  if (options.dryRun) {
455222
- outputResult({
455223
- dryRun: true,
455840
+ outputSuccess({
455224
455841
  command: "generate sound-effect",
455225
- params: {
455226
- prompt: prompt3,
455227
- duration: options.duration,
455228
- promptInfluence: options.promptInfluence,
455229
- output: options.output
455842
+ startedAt,
455843
+ dryRun: true,
455844
+ data: {
455845
+ params: {
455846
+ prompt: prompt3,
455847
+ duration: options.duration,
455848
+ promptInfluence: options.promptInfluence,
455849
+ output: options.output
455850
+ }
455230
455851
  }
455231
455852
  });
455232
455853
  return;
@@ -455253,7 +455874,11 @@ function registerSoundEffectCommand(parent) {
455253
455874
  await writeFile22(outputPath, result.audioBuffer);
455254
455875
  spinner2.succeed(source_default.green("Sound effect generated"));
455255
455876
  if (isJsonMode()) {
455256
- outputResult({ success: true, outputPath });
455877
+ outputSuccess({
455878
+ command: "generate sound-effect",
455879
+ startedAt,
455880
+ data: { outputPath }
455881
+ });
455257
455882
  return;
455258
455883
  }
455259
455884
  console.log(source_default.green(`Saved to: ${outputPath}`));
@@ -455303,6 +455928,7 @@ async function executeMusicStatus(options) {
455303
455928
  }
455304
455929
  function registerMusicStatusCommand(parent) {
455305
455930
  parent.command("music-status", { hidden: true }).description("Check music generation status").argument("<task-id>", "Task ID from music generation").option("-k, --api-key <key>", "Replicate API token (or set REPLICATE_API_TOKEN env)").action(async (taskId, options) => {
455931
+ const startedAt = Date.now();
455306
455932
  try {
455307
455933
  const apiKey = await requireApiKey(
455308
455934
  "REPLICATE_API_TOKEN",
@@ -455314,12 +455940,15 @@ function registerMusicStatusCommand(parent) {
455314
455940
  const result = await replicate.getMusicStatus(taskId);
455315
455941
  if (isJsonMode()) {
455316
455942
  const status = result.audioUrl ? "completed" : result.error ? "failed" : "processing";
455317
- outputResult({
455318
- success: true,
455319
- taskId,
455320
- status,
455321
- audioUrl: result.audioUrl,
455322
- error: result.error
455943
+ outputSuccess({
455944
+ command: "generate music-status",
455945
+ startedAt,
455946
+ data: {
455947
+ taskId,
455948
+ status,
455949
+ audioUrl: result.audioUrl,
455950
+ error: result.error
455951
+ }
455323
455952
  });
455324
455953
  return;
455325
455954
  }
@@ -455357,6 +455986,7 @@ var init_music_status = __esm({
455357
455986
  // ../cli/src/commands/generate/video-cancel.ts
455358
455987
  function registerVideoCancelCommand(parent) {
455359
455988
  parent.command("video-cancel", { hidden: true }).description("Cancel video generation (Grok or Runway)").argument("<task-id>", "Task ID to cancel").option("-p, --provider <provider>", "Provider: grok, runway", "grok").option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET env)").action(async (taskId, options) => {
455989
+ const startedAt = Date.now();
455360
455990
  try {
455361
455991
  const provider = (options.provider || "grok").toLowerCase();
455362
455992
  let success = false;
@@ -455369,7 +455999,11 @@ function registerVideoCancelCommand(parent) {
455369
455999
  if (success) {
455370
456000
  spinner2.succeed(source_default.green("Generation cancelled"));
455371
456001
  if (isJsonMode()) {
455372
- outputResult({ success: true, taskId, provider: "grok", cancelled: true });
456002
+ outputSuccess({
456003
+ command: "generate video-cancel",
456004
+ startedAt,
456005
+ data: { taskId, provider: "grok", cancelled: true }
456006
+ });
455373
456007
  return;
455374
456008
  }
455375
456009
  } else {
@@ -455389,7 +456023,11 @@ function registerVideoCancelCommand(parent) {
455389
456023
  if (success) {
455390
456024
  spinner2.succeed(source_default.green("Generation cancelled"));
455391
456025
  if (isJsonMode()) {
455392
- outputResult({ success: true, taskId, provider: "runway", cancelled: true });
456026
+ outputSuccess({
456027
+ command: "generate video-cancel",
456028
+ startedAt,
456029
+ data: { taskId, provider: "runway", cancelled: true }
456030
+ });
455393
456031
  return;
455394
456032
  }
455395
456033
  } else {
@@ -455462,16 +456100,18 @@ async function executeBackground(options) {
455462
456100
  }
455463
456101
  function registerBackgroundCommand(parent) {
455464
456102
  parent.command("background").description("Generate video background using DALL-E").argument("<description>", "Background description").option("-k, --api-key <key>", "OpenAI API key (or set OPENAI_API_KEY env)").option("-o, --output <path>", "Output file path (downloads image)").option("-a, --aspect <ratio>", "Aspect ratio: 16:9, 9:16, 1:1", "16:9").option("--dry-run", "Preview parameters without executing").action(async (description, options) => {
456103
+ const startedAt = Date.now();
455465
456104
  try {
455466
456105
  rejectControlChars(description);
455467
456106
  if (options.output) {
455468
456107
  validateOutputPath(options.output);
455469
456108
  }
455470
456109
  if (options.dryRun) {
455471
- outputResult({
455472
- dryRun: true,
456110
+ outputSuccess({
455473
456111
  command: "generate background",
455474
- params: { description, aspect: options.aspect, output: options.output }
456112
+ startedAt,
456113
+ dryRun: true,
456114
+ data: { params: { description, aspect: options.aspect, output: options.output } }
455475
456115
  });
455476
456116
  return;
455477
456117
  }
@@ -455506,7 +456146,11 @@ function registerBackgroundCommand(parent) {
455506
456146
  await mkdir18(dirname25(outputPath), { recursive: true });
455507
456147
  await writeFile23(outputPath, buffer);
455508
456148
  }
455509
- outputResult({ success: true, imageUrl: img.url, outputPath });
456149
+ outputSuccess({
456150
+ command: "generate background",
456151
+ startedAt,
456152
+ data: { imageUrl: img.url, outputPath }
456153
+ });
455510
456154
  return;
455511
456155
  }
455512
456156
  console.log();
@@ -455603,6 +456247,7 @@ async function executeStoryboard(options) {
455603
456247
  }
455604
456248
  function registerStoryboardCommand(parent) {
455605
456249
  parent.command("storyboard").description("Generate video storyboard from content using Claude").argument("<content>", "Content to analyze (text or file path)").option("-k, --api-key <key>", "Anthropic API key (or set ANTHROPIC_API_KEY env)").option("-o, --output <path>", "Output JSON file path").option("-d, --duration <sec>", "Target total duration in seconds").option("-f, --file", "Treat content argument as file path").option("-c, --creativity <level>", "Creativity level: low (default, consistent) or high (varied, unexpected)", "low").option("--dry-run", "Preview parameters without executing").action(async (content, options) => {
456250
+ const startedAt = Date.now();
455606
456251
  try {
455607
456252
  rejectControlChars(content);
455608
456253
  if (options.output) {
@@ -455618,13 +456263,16 @@ function registerStoryboardCommand(parent) {
455618
456263
  textContent2 = await readFile23(filePath, "utf-8");
455619
456264
  }
455620
456265
  if (options.dryRun) {
455621
- outputResult({
455622
- dryRun: true,
456266
+ outputSuccess({
455623
456267
  command: "generate storyboard",
455624
- params: {
455625
- content: textContent2.substring(0, 200),
455626
- duration: options.duration,
455627
- creativity
456268
+ startedAt,
456269
+ dryRun: true,
456270
+ data: {
456271
+ params: {
456272
+ content: textContent2.substring(0, 200),
456273
+ duration: options.duration,
456274
+ creativity
456275
+ }
455628
456276
  }
455629
456277
  });
455630
456278
  return;
@@ -455656,16 +456304,23 @@ function registerStoryboardCommand(parent) {
455656
456304
  const outputPath = resolve39(process.cwd(), options.output);
455657
456305
  await writeFile24(outputPath, JSON.stringify(segments, null, 2), "utf-8");
455658
456306
  if (isJsonMode()) {
455659
- outputResult({
455660
- success: true,
455661
- segmentCount: segments.length,
455662
- segments,
455663
- outputPath
456307
+ outputSuccess({
456308
+ command: "generate storyboard",
456309
+ startedAt,
456310
+ data: {
456311
+ segmentCount: segments.length,
456312
+ segments,
456313
+ outputPath
456314
+ }
455664
456315
  });
455665
456316
  return;
455666
456317
  }
455667
456318
  } else if (isJsonMode()) {
455668
- outputResult({ success: true, segmentCount: segments.length, segments });
456319
+ outputSuccess({
456320
+ command: "generate storyboard",
456321
+ startedAt,
456322
+ data: { segmentCount: segments.length, segments }
456323
+ });
455669
456324
  return;
455670
456325
  }
455671
456326
  console.log();
@@ -455797,6 +456452,7 @@ async function executeSpeech(options) {
455797
456452
  }
455798
456453
  function registerSpeechCommand(parent) {
455799
456454
  parent.command("speech").alias("tts").description("Generate speech from text using ElevenLabs").argument("[text]", "Text to convert to speech (interactive if omitted)").option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)").option("-o, --output <path>", "Output audio file path", "output.mp3").option("-v, --voice <id>", "Voice ID (default: Rachel)", "21m00Tcm4TlvDq8ikWAM").option("--list-voices", "List available voices").option("--fit-duration <seconds>", "Speed up audio to fit target duration (via FFmpeg atempo)", parseFloat).option("--dry-run", "Preview parameters without executing").action(async (text, options) => {
456455
+ const startedAt = Date.now();
455800
456456
  try {
455801
456457
  if (!text) {
455802
456458
  if (hasTTY()) {
@@ -455818,10 +456474,11 @@ function registerSpeechCommand(parent) {
455818
456474
  validateOutputPath(options.output);
455819
456475
  }
455820
456476
  if (options.dryRun) {
455821
- outputResult({
455822
- dryRun: true,
456477
+ outputSuccess({
455823
456478
  command: "generate speech",
455824
- params: { text, voice: options.voice, output: options.output }
456479
+ startedAt,
456480
+ dryRun: true,
456481
+ data: { params: { text, voice: options.voice, output: options.output } }
455825
456482
  });
455826
456483
  return;
455827
456484
  }
@@ -455910,10 +456567,13 @@ function registerSpeechCommand(parent) {
455910
456567
  }
455911
456568
  }
455912
456569
  if (isJsonMode()) {
455913
- outputResult({
455914
- success: true,
455915
- characterCount: result.characterCount,
455916
- outputPath
456570
+ outputSuccess({
456571
+ command: "generate speech",
456572
+ startedAt,
456573
+ data: {
456574
+ characterCount: result.characterCount,
456575
+ outputPath
456576
+ }
455917
456577
  });
455918
456578
  return;
455919
456579
  }
@@ -456002,6 +456662,7 @@ async function executeMusic(options) {
456002
456662
  }
456003
456663
  function registerMusicCommand(parent) {
456004
456664
  parent.command("music").description("Generate background music from a text prompt (ElevenLabs or Replicate MusicGen)").argument("<prompt>", "Description of the music to generate").option("-p, --provider <provider>", "Provider: elevenlabs (default, up to 10min), replicate (MusicGen, max 30s)", "elevenlabs").option("-k, --api-key <key>", "API key (or set ELEVENLABS_API_KEY / REPLICATE_API_TOKEN env)").option("-d, --duration <seconds>", "Duration in seconds (elevenlabs: 3-600, replicate: 1-30)", "8").option("--instrumental", "Force instrumental music, no vocals (ElevenLabs only)").option("-m, --melody <file>", "Reference melody audio file for conditioning (Replicate only)").option("--model <model>", "Model variant (Replicate only): large, stereo-large, melody-large, stereo-melody-large", "stereo-large").option("-o, --output <path>", "Output audio file path", "music.mp3").option("--no-wait", "Don't wait for generation to complete (Replicate async mode)").option("--dry-run", "Preview parameters without executing").action(async (prompt3, options) => {
456665
+ const startedAt = Date.now();
456005
456666
  try {
456006
456667
  rejectControlChars(prompt3);
456007
456668
  if (options.output) {
@@ -456009,16 +456670,19 @@ function registerMusicCommand(parent) {
456009
456670
  }
456010
456671
  const provider = (options.provider || "elevenlabs").toLowerCase();
456011
456672
  if (options.dryRun) {
456012
- outputResult({
456013
- dryRun: true,
456673
+ outputSuccess({
456014
456674
  command: "generate music",
456015
- params: {
456016
- prompt: prompt3,
456017
- provider,
456018
- duration: options.duration,
456019
- model: options.model,
456020
- output: options.output,
456021
- instrumental: options.instrumental
456675
+ startedAt,
456676
+ dryRun: true,
456677
+ data: {
456678
+ params: {
456679
+ prompt: prompt3,
456680
+ provider,
456681
+ duration: options.duration,
456682
+ model: options.model,
456683
+ output: options.output,
456684
+ instrumental: options.instrumental
456685
+ }
456022
456686
  }
456023
456687
  });
456024
456688
  return;
@@ -456045,11 +456709,14 @@ function registerMusicCommand(parent) {
456045
456709
  await writeFile26(outputPath, result.audioBuffer);
456046
456710
  spinner2.succeed(source_default.green("Music generated successfully"));
456047
456711
  if (isJsonMode()) {
456048
- outputResult({
456049
- success: true,
456050
- provider: "elevenlabs",
456051
- outputPath,
456052
- duration
456712
+ outputSuccess({
456713
+ command: "generate music",
456714
+ startedAt,
456715
+ data: {
456716
+ provider: "elevenlabs",
456717
+ outputPath,
456718
+ duration
456719
+ }
456053
456720
  });
456054
456721
  return;
456055
456722
  }
@@ -456117,12 +456784,15 @@ function registerMusicCommand(parent) {
456117
456784
  await writeFile26(outputPath, audioBuffer);
456118
456785
  spinner2.succeed(source_default.green("Music generated successfully"));
456119
456786
  if (isJsonMode()) {
456120
- outputResult({
456121
- success: true,
456122
- provider: "replicate",
456123
- taskId: result.taskId,
456124
- audioUrl: finalResult.audioUrl,
456125
- outputPath
456787
+ outputSuccess({
456788
+ command: "generate music",
456789
+ startedAt,
456790
+ data: {
456791
+ provider: "replicate",
456792
+ taskId: result.taskId,
456793
+ audioUrl: finalResult.audioUrl,
456794
+ outputPath
456795
+ }
456126
456796
  });
456127
456797
  return;
456128
456798
  }
@@ -456157,6 +456827,7 @@ import { existsSync as existsSync47 } from "node:fs";
456157
456827
  import { writeFile as writeFile27, mkdir as mkdir19 } from "node:fs/promises";
456158
456828
  function registerThumbnailCommand(parent) {
456159
456829
  parent.command("thumbnail").description("Generate video thumbnail (DALL-E) or extract best frame from video (Gemini)").argument("[description]", "Thumbnail description (for DALL-E generation)").option("-k, --api-key <key>", "API key (OpenAI for generation, Google for best-frame)").option("-o, --output <path>", "Output file path").option("-s, --style <style>", "Platform style: youtube, instagram, tiktok, twitter").option("--best-frame <video>", "Extract best thumbnail frame from video using Gemini AI").option("--prompt <prompt>", "Custom prompt for best-frame analysis").option("--model <model>", "Gemini model: flash, latest, pro (default: flash)", "flash").action(async (description, options) => {
456830
+ const startedAt = Date.now();
456160
456831
  try {
456161
456832
  if (description) rejectControlChars(description);
456162
456833
  if (options.output) {
@@ -456192,11 +456863,14 @@ function registerThumbnailCommand(parent) {
456192
456863
  }
456193
456864
  spinner3.succeed(source_default.green("Best frame extracted"));
456194
456865
  if (isJsonMode()) {
456195
- outputResult({
456196
- success: true,
456197
- timestamp: result2.timestamp,
456198
- reason: result2.reason,
456199
- outputPath: result2.outputPath
456866
+ outputSuccess({
456867
+ command: "generate thumbnail",
456868
+ startedAt,
456869
+ data: {
456870
+ timestamp: result2.timestamp,
456871
+ reason: result2.reason,
456872
+ outputPath: result2.outputPath
456873
+ }
456200
456874
  });
456201
456875
  return;
456202
456876
  }
@@ -456244,7 +456918,11 @@ function registerThumbnailCommand(parent) {
456244
456918
  await mkdir19(dirname26(outputPath), { recursive: true });
456245
456919
  await writeFile27(outputPath, buffer);
456246
456920
  }
456247
- outputResult({ success: true, imageUrl: img.url, outputPath });
456921
+ outputSuccess({
456922
+ command: "generate thumbnail",
456923
+ startedAt,
456924
+ data: { imageUrl: img.url, outputPath }
456925
+ });
456248
456926
  return;
456249
456927
  }
456250
456928
  console.log();
@@ -456315,6 +456993,7 @@ function getStatusColor(status) {
456315
456993
  }
456316
456994
  function registerVideoStatusCommand(parent) {
456317
456995
  parent.command("video-status", { hidden: true }).description("Check video generation status (Grok, Runway, or Kling)").argument("<task-id>", "Task ID from video generation").option("-p, --provider <provider>", "Provider: grok, runway, kling", "grok").option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY env)").option("-t, --type <type>", "Task type: text2video or image2video (Kling only)", "text2video").option("-w, --wait", "Wait for completion").option("-o, --output <path>", "Download video when complete").action(async (taskId, options) => {
456996
+ const startedAt = Date.now();
456318
456997
  try {
456319
456998
  const provider = (options.provider || "grok").toLowerCase();
456320
456999
  if (provider === "grok") {
@@ -456337,14 +457016,17 @@ function registerVideoStatusCommand(parent) {
456337
457016
  outputPath = resolve44(process.cwd(), options.output);
456338
457017
  await writeFile28(outputPath, buffer);
456339
457018
  }
456340
- outputResult({
456341
- success: true,
456342
- taskId,
456343
- provider: "grok",
456344
- status: result.status,
456345
- videoUrl: result.videoUrl,
456346
- error: result.error,
456347
- outputPath
457019
+ outputSuccess({
457020
+ command: "generate video-status",
457021
+ startedAt,
457022
+ data: {
457023
+ taskId,
457024
+ provider: "grok",
457025
+ status: result.status,
457026
+ videoUrl: result.videoUrl,
457027
+ error: result.error,
457028
+ outputPath
457029
+ }
456348
457030
  });
456349
457031
  return;
456350
457032
  }
@@ -456402,15 +457084,18 @@ function registerVideoStatusCommand(parent) {
456402
457084
  outputPath = resolve44(process.cwd(), options.output);
456403
457085
  await writeFile28(outputPath, buffer);
456404
457086
  }
456405
- outputResult({
456406
- success: true,
456407
- taskId,
456408
- provider: "runway",
456409
- status: result.status,
456410
- videoUrl: result.videoUrl,
456411
- progress: result.progress,
456412
- error: result.error,
456413
- outputPath
457087
+ outputSuccess({
457088
+ command: "generate video-status",
457089
+ startedAt,
457090
+ data: {
457091
+ taskId,
457092
+ provider: "runway",
457093
+ status: result.status,
457094
+ videoUrl: result.videoUrl,
457095
+ progress: result.progress,
457096
+ error: result.error,
457097
+ outputPath
457098
+ }
456414
457099
  });
456415
457100
  return;
456416
457101
  }
@@ -456466,15 +457151,18 @@ function registerVideoStatusCommand(parent) {
456466
457151
  outputPath = resolve44(process.cwd(), options.output);
456467
457152
  await writeFile28(outputPath, buffer);
456468
457153
  }
456469
- outputResult({
456470
- success: true,
456471
- taskId,
456472
- provider: "kling",
456473
- status: result.status,
456474
- videoUrl: result.videoUrl,
456475
- duration: result.duration,
456476
- error: result.error,
456477
- outputPath
457154
+ outputSuccess({
457155
+ command: "generate video-status",
457156
+ startedAt,
457157
+ data: {
457158
+ taskId,
457159
+ provider: "kling",
457160
+ status: result.status,
457161
+ videoUrl: result.videoUrl,
457162
+ duration: result.duration,
457163
+ error: result.error,
457164
+ outputPath
457165
+ }
456478
457166
  });
456479
457167
  return;
456480
457168
  }
@@ -456538,22 +457226,26 @@ import { resolve as resolve45 } from "node:path";
456538
457226
  import { writeFile as writeFile29 } from "node:fs/promises";
456539
457227
  function registerVideoExtendCommand(parent) {
456540
457228
  parent.command("video-extend", { hidden: true }).description("Extend video duration (Kling by video ID, Veo by operation name)").argument("<id>", "Kling video ID or Veo operation name").option("-p, --provider <provider>", "Provider: kling, veo", "kling").option("-k, --api-key <key>", "API key (KLING_API_KEY or GOOGLE_API_KEY)").option("-o, --output <path>", "Output file path").option("--prompt <text>", "Continuation prompt").option("-d, --duration <sec>", "Duration: 5 or 10 (Kling), 4/6/8 (Veo)", "5").option("-n, --negative <prompt>", "Negative prompt (what to avoid, Kling only)").option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast", "3.1").option("--no-wait", "Start extension and return task ID without waiting").option("--dry-run", "Preview parameters without executing").action(async (id, options) => {
457229
+ const startedAt = Date.now();
456541
457230
  try {
456542
457231
  const provider = (options.provider || "kling").toLowerCase();
456543
457232
  if (options.output) {
456544
457233
  validateOutputPath(options.output);
456545
457234
  }
456546
457235
  if (options.dryRun) {
456547
- outputResult({
456548
- dryRun: true,
457236
+ outputSuccess({
456549
457237
  command: "generate video-extend",
456550
- params: {
456551
- id,
456552
- provider,
456553
- prompt: options.prompt,
456554
- duration: options.duration,
456555
- negative: options.negative,
456556
- veoModel: options.veoModel
457238
+ startedAt,
457239
+ dryRun: true,
457240
+ data: {
457241
+ params: {
457242
+ id,
457243
+ provider,
457244
+ prompt: options.prompt,
457245
+ duration: options.duration,
457246
+ negative: options.negative,
457247
+ veoModel: options.veoModel
457248
+ }
456557
457249
  }
456558
457250
  });
456559
457251
  return;
@@ -456610,13 +457302,16 @@ function registerVideoExtendCommand(parent) {
456610
457302
  outputPath = resolve45(process.cwd(), options.output);
456611
457303
  await writeFile29(outputPath, buffer);
456612
457304
  }
456613
- outputResult({
456614
- success: true,
456615
- provider: "kling",
456616
- taskId: result.id,
456617
- videoUrl: finalResult.videoUrl,
456618
- duration: finalResult.duration,
456619
- outputPath
457305
+ outputSuccess({
457306
+ command: "generate video-extend",
457307
+ startedAt,
457308
+ data: {
457309
+ provider: "kling",
457310
+ taskId: result.id,
457311
+ videoUrl: finalResult.videoUrl,
457312
+ duration: finalResult.duration,
457313
+ outputPath
457314
+ }
456620
457315
  });
456621
457316
  return;
456622
457317
  }
@@ -456696,13 +457391,16 @@ function registerVideoExtendCommand(parent) {
456696
457391
  outputPath = resolve45(process.cwd(), options.output);
456697
457392
  await writeFile29(outputPath, buffer);
456698
457393
  }
456699
- outputResult({
456700
- success: true,
456701
- provider: "veo",
456702
- taskId: result.id,
456703
- videoUrl: finalResult.videoUrl,
456704
- duration: finalResult.duration,
456705
- outputPath
457394
+ outputSuccess({
457395
+ command: "generate video-extend",
457396
+ startedAt,
457397
+ data: {
457398
+ provider: "veo",
457399
+ taskId: result.id,
457400
+ videoUrl: finalResult.videoUrl,
457401
+ duration: finalResult.duration,
457402
+ outputPath
457403
+ }
456706
457404
  });
456707
457405
  return;
456708
457406
  }
@@ -456817,6 +457515,7 @@ Examples:
456817
457515
  $ vibe gen img "landscape photo" -o wide.png -r 16:9
456818
457516
  $ vibe gen img "portrait" -o portrait.png -p gemini -m pro
456819
457517
  $ vibe gen img "product shot" --dry-run --json`).action(async (prompt3, options) => {
457518
+ const startedAt = Date.now();
456820
457519
  try {
456821
457520
  if (!prompt3) {
456822
457521
  if (hasTTY()) {
@@ -456875,18 +457574,21 @@ Examples:
456875
457574
  provider = resolved?.name ?? "gemini";
456876
457575
  }
456877
457576
  if (options.dryRun) {
456878
- outputResult({
456879
- dryRun: true,
457577
+ outputSuccess({
456880
457578
  command: "generate image",
456881
- params: {
456882
- prompt: prompt3,
456883
- provider,
456884
- model: options.model,
456885
- ratio: options.ratio,
456886
- size: options.size,
456887
- quality: options.quality,
456888
- count: options.count,
456889
- output: options.output
457579
+ startedAt,
457580
+ dryRun: true,
457581
+ data: {
457582
+ params: {
457583
+ prompt: prompt3,
457584
+ provider,
457585
+ model: options.model,
457586
+ ratio: options.ratio,
457587
+ size: options.size,
457588
+ quality: options.quality,
457589
+ count: options.count,
457590
+ output: options.output
457591
+ }
456890
457592
  }
456891
457593
  });
456892
457594
  return;
@@ -456924,14 +457626,17 @@ Examples:
456924
457626
  await mkdir20(dirname27(outputPath), { recursive: true });
456925
457627
  await writeFile30(outputPath, buffer);
456926
457628
  }
456927
- outputResult({
456928
- success: true,
456929
- provider: "openai",
456930
- images: result.images.map((img) => ({
456931
- url: img.url,
456932
- revisedPrompt: img.revisedPrompt
456933
- })),
456934
- outputPath
457629
+ outputSuccess({
457630
+ command: "generate image",
457631
+ startedAt,
457632
+ data: {
457633
+ provider: "openai",
457634
+ images: result.images.map((img) => ({
457635
+ url: img.url,
457636
+ revisedPrompt: img.revisedPrompt
457637
+ })),
457638
+ outputPath
457639
+ }
456935
457640
  });
456936
457641
  return;
456937
457642
  }
@@ -457043,13 +457748,18 @@ Examples:
457043
457748
  await mkdir20(dirname27(outputPath), { recursive: true });
457044
457749
  await writeFile30(outputPath, buffer);
457045
457750
  }
457046
- outputResult({
457047
- success: true,
457048
- provider: "gemini",
457049
- images: result.images.map((img) => ({
457050
- mimeType: img.mimeType
457051
- })),
457052
- outputPath
457751
+ outputSuccess({
457752
+ command: "generate image",
457753
+ startedAt,
457754
+ warnings: usedLabel.includes("fallback") ? [`Model "${options.model}" failed; fell back to flash`] : [],
457755
+ data: {
457756
+ provider: "gemini",
457757
+ model: usedLabel,
457758
+ images: result.images.map((img) => ({
457759
+ mimeType: img.mimeType
457760
+ })),
457761
+ outputPath
457762
+ }
457053
457763
  });
457054
457764
  return;
457055
457765
  }
@@ -457133,11 +457843,14 @@ Examples:
457133
457843
  await mkdir20(dirname27(outputPath), { recursive: true });
457134
457844
  await writeFile30(outputPath, buffer);
457135
457845
  }
457136
- outputResult({
457137
- success: true,
457138
- provider: "grok",
457139
- images: result.images.map((img) => ({ url: img.url })),
457140
- outputPath
457846
+ outputSuccess({
457847
+ command: "generate image",
457848
+ startedAt,
457849
+ data: {
457850
+ provider: "grok",
457851
+ images: result.images.map((img) => ({ url: img.url })),
457852
+ outputPath
457853
+ }
457141
457854
  });
457142
457855
  return;
457143
457856
  }
@@ -457208,11 +457921,14 @@ Examples:
457208
457921
  proc.on("close", (code) => {
457209
457922
  if (code === 0) {
457210
457923
  if (isJsonMode()) {
457211
- outputResult({
457212
- success: true,
457213
- provider: "runway",
457214
- images: [{ format: "file" }],
457215
- outputPath
457924
+ outputSuccess({
457925
+ command: "generate image",
457926
+ startedAt,
457927
+ data: {
457928
+ provider: "runway",
457929
+ images: [{ format: "file" }],
457930
+ outputPath
457931
+ }
457216
457932
  });
457217
457933
  } else {
457218
457934
  spinner2.succeed(source_default.green("Generated image with Runway"));
@@ -458931,13 +459647,19 @@ var init_ai_script_pipeline = __esm({
458931
459647
  import { resolve as resolve49 } from "node:path";
458932
459648
  import { readFile as readFile25, writeFile as writeFile33 } from "node:fs/promises";
458933
459649
  function registerVideoCommand(parent) {
458934
- parent.command("video").alias("vid").description("Generate video using AI (Kling, Runway, Veo, or Grok)").argument("[prompt]", "Text prompt describing the video (interactive if omitted)").option("-p, --provider <provider>", "Provider: fal (Seedance 2.0, default when FAL_KEY set), grok, kling, runway, veo").option("-k, --api-key <key>", "API key (or set XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY / GOOGLE_API_KEY env)").option("-o, --output <path>", "Output file path (downloads video)").option("-i, --image <path>", "Reference image for image-to-video").option("-d, --duration <sec>", "Duration: 5 or 10 seconds", "5").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, or 1:1 (auto-detected from image if omitted)").option("-s, --seed <number>", "Random seed for reproducibility (Runway only)").option("-m, --mode <mode>", "Generation mode: std or pro (Kling only)", "std").option("-n, --negative <prompt>", "Negative prompt - what to avoid (Kling/Veo)").option("--resolution <res>", "Video resolution: 720p, 1080p, 4k (Veo only)").option("--last-frame <path>", "Last frame image for frame interpolation (Veo only)").option("--ref-images <paths...>", "Reference images for character consistency (Veo 3.1 only, max 3)").option("--person <mode>", "Person generation: allow_all, allow_adult (Veo only)").option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast (default: 3.1-fast)", "3.1-fast").option("--runway-model <model>", "Runway model: gen4.5 (default, text+image-to-video), gen4_turbo (image-to-video only)", "gen4.5").option("--no-wait", "Start generation and return task ID without waiting").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
459650
+ parent.command("video").alias("vid").description("Generate video using AI (Seedance, Grok, Kling, Runway, or Veo)").argument("[prompt]", "Text prompt describing the video (interactive if omitted)").option("-p, --provider <provider>", "Provider: seedance (ByteDance Seedance 2.0 via fal.ai), grok, kling, runway, veo. `fal` is a backwards-compatible alias for seedance.").option("-k, --api-key <key>", "API key (or set FAL_KEY / XAI_API_KEY / RUNWAY_API_SECRET / KLING_API_KEY / GOOGLE_API_KEY env)").option("-o, --output <path>", "Output file path (downloads video)").option("-i, --image <path>", "Reference image for image-to-video").option(
459651
+ "-d, --duration <sec>",
459652
+ "Duration in seconds. Seedance accepts 4-15 (`fal` alias supported); Kling accepts 5 or 10; Veo maps to 6 or 8.",
459653
+ "5"
459654
+ ).option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, or 1:1 (auto-detected from image if omitted)").option("-s, --seed <number>", "Random seed for reproducibility (Runway only)").option("-m, --mode <mode>", "Generation mode: std or pro (Kling only)", "std").option("--seedance-model <model>", "Seedance variant: quality or fast (fal.ai only)", "quality").option("-n, --negative <prompt>", "Negative prompt - what to avoid (Kling/Veo)").option("--resolution <res>", "Video resolution: 720p, 1080p, 4k (Veo only)").option("--last-frame <path>", "Last frame image for frame interpolation (Veo only)").option("--ref-images <paths...>", "Reference images for character consistency (Veo 3.1 only, max 3)").option("--person <mode>", "Person generation: allow_all, allow_adult (Veo only)").option("--veo-model <model>", "Veo model: 3.0, 3.1, 3.1-fast (default: 3.1-fast)", "3.1-fast").option("--runway-model <model>", "Runway model: gen4.5 (default, text+image-to-video), gen4_turbo (image-to-video only)", "gen4.5").option("--no-wait", "Start generation and return task ID without waiting").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
458935
459655
  Examples:
458936
- $ vibe generate video "dancing cat" -o cat.mp4 # Grok (default)
459656
+ $ vibe generate video "dancing cat" -o cat.mp4 # Seedance when FAL_KEY is set
459657
+ $ vibe gen vid "cinematic city timelapse" -o city.mp4 -p seedance # Seedance via fal.ai
458937
459658
  $ vibe gen vid "city timelapse" -o city.mp4 -p kling # Kling
458938
459659
  $ vibe gen vid "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
458939
459660
  $ vibe gen vid "ocean waves" -o waves.mp4 -p veo --resolution 1080p # Veo
458940
459661
  $ vibe gen vid "sunset" -o sun.mp4 -d 10 --dry-run --json`).action(async (prompt3, options) => {
459662
+ const startedAt = Date.now();
458941
459663
  try {
458942
459664
  if (!prompt3) {
458943
459665
  if (hasTTY()) {
@@ -458958,12 +459680,13 @@ Examples:
458958
459680
  if (options.output) {
458959
459681
  validateOutputPath(options.output);
458960
459682
  }
458961
- const validProviders = ["runway", "kling", "veo", "grok", "fal"];
459683
+ const validProviders = ["runway", "kling", "veo", "grok", "seedance", "fal"];
458962
459684
  const videoEnvMap = {
458963
459685
  grok: "XAI_API_KEY",
458964
459686
  veo: "GOOGLE_API_KEY",
458965
459687
  kling: "KLING_API_KEY",
458966
459688
  runway: "RUNWAY_API_SECRET",
459689
+ seedance: "FAL_KEY",
458967
459690
  fal: "FAL_KEY"
458968
459691
  };
458969
459692
  let provider;
@@ -458973,7 +459696,7 @@ Examples:
458973
459696
  exitWithError(
458974
459697
  usageError(
458975
459698
  `Invalid provider: ${provider}`,
458976
- `Available providers: ${validProviders.join(", ")}`
459699
+ "Available providers: seedance, grok, kling, runway, veo. `fal` is a backwards-compatible alias for seedance."
458977
459700
  )
458978
459701
  );
458979
459702
  }
@@ -459029,19 +459752,23 @@ Examples:
459029
459752
  options.ratio = "16:9";
459030
459753
  }
459031
459754
  if (options.dryRun) {
459032
- outputResult({
459033
- dryRun: true,
459755
+ outputSuccess({
459034
459756
  command: "generate video",
459035
- params: {
459036
- prompt: prompt3,
459037
- provider,
459038
- duration: options.duration,
459039
- ratio: options.ratio,
459040
- image: options.image,
459041
- mode: options.mode,
459042
- negative: options.negative,
459043
- resolution: options.resolution,
459044
- veoModel: options.veoModel
459757
+ startedAt,
459758
+ dryRun: true,
459759
+ data: {
459760
+ params: {
459761
+ prompt: prompt3,
459762
+ provider: provider === "fal" ? "seedance" : provider,
459763
+ duration: options.duration,
459764
+ ratio: options.ratio,
459765
+ image: options.image,
459766
+ mode: options.mode,
459767
+ negative: options.negative,
459768
+ resolution: options.resolution,
459769
+ veoModel: options.veoModel,
459770
+ seedanceModel: options.seedanceModel
459771
+ }
459045
459772
  }
459046
459773
  });
459047
459774
  return;
@@ -459051,6 +459778,7 @@ Examples:
459051
459778
  kling: "KLING_API_KEY",
459052
459779
  veo: "GOOGLE_API_KEY",
459053
459780
  grok: "XAI_API_KEY",
459781
+ seedance: "FAL_KEY",
459054
459782
  fal: "FAL_KEY"
459055
459783
  };
459056
459784
  const providerNameMap = {
@@ -459058,7 +459786,8 @@ Examples:
459058
459786
  kling: "Kling",
459059
459787
  veo: "Veo",
459060
459788
  grok: "Grok",
459061
- fal: "fal.ai (Seedance 2.0)"
459789
+ seedance: "Seedance 2.0 via fal.ai",
459790
+ fal: "Seedance 2.0 via fal.ai"
459062
459791
  };
459063
459792
  const envKey = envKeyMap[provider];
459064
459793
  const providerName = providerNameMap[provider];
@@ -459277,15 +460006,15 @@ Examples:
459277
460006
  },
459278
460007
  3e5
459279
460008
  );
459280
- } else if (provider === "fal") {
460009
+ } else if (provider === "fal" || provider === "seedance") {
459281
460010
  const fal = new FalProvider();
459282
460011
  await fal.initialize({ apiKey });
459283
460012
  let falImage = referenceImage;
459284
460013
  if (falImage && falImage.startsWith("data:")) {
459285
- spinner2.text = "Uploading image to ImgBB for fal...";
460014
+ spinner2.text = "Uploading image to ImgBB for Seedance...";
459286
460015
  const imgbbKey = await getApiKeyFromConfig("imgbb") || process.env.IMGBB_API_KEY;
459287
460016
  if (!imgbbKey) {
459288
- spinner2.fail("ImgBB API key required for fal image-to-video");
460017
+ spinner2.fail("ImgBB API key required for Seedance image-to-video");
459289
460018
  exitWithError(authError("IMGBB_API_KEY", "ImgBB"));
459290
460019
  }
459291
460020
  const base64Data = falImage.split(",")[1];
@@ -459297,8 +460026,9 @@ Examples:
459297
460026
  }
459298
460027
  falImage = uploadResult.url;
459299
460028
  }
459300
- spinner2.text = "Generating video with Seedance 2.0 (this may take 1-3 minutes)...";
459301
- const falModel = options.model === "fast" ? "seedance-2.0-fast" : "seedance-2.0";
460029
+ spinner2.text = "Generating video with fal.ai Seedance 2.0 (this may take 1-3 minutes)...";
460030
+ const seedanceModel = String(options.seedanceModel ?? "quality").toLowerCase();
460031
+ const falModel = seedanceModel === "fast" || seedanceModel === "seedance-2.0-fast" ? "seedance-2.0-fast" : "seedance-2.0";
459302
460032
  result = await fal.generateVideo(prompt3, {
459303
460033
  prompt: prompt3,
459304
460034
  referenceImage: falImage,
@@ -459321,13 +460051,16 @@ Examples:
459321
460051
  outputPath = resolve49(process.cwd(), options.output);
459322
460052
  await writeFile33(outputPath, buffer);
459323
460053
  }
459324
- outputResult({
459325
- success: true,
459326
- provider,
459327
- taskId: result?.id,
459328
- videoUrl: finalResult.videoUrl,
459329
- duration: finalResult.duration,
459330
- outputPath
460054
+ outputSuccess({
460055
+ command: "generate video",
460056
+ startedAt,
460057
+ data: {
460058
+ provider,
460059
+ taskId: result?.id,
460060
+ videoUrl: finalResult.videoUrl,
460061
+ duration: finalResult.duration,
460062
+ outputPath
460063
+ }
459331
460064
  });
459332
460065
  return;
459333
460066
  }
@@ -459420,7 +460153,8 @@ var init_generate = __esm({
459420
460153
  Examples:
459421
460154
  $ vibe generate image "a sunset over the ocean" -o sunset.png
459422
460155
  $ vibe generate image "logo design" -o logo.png -p openai
459423
- $ vibe generate video "dancing cat" -o cat.mp4 # Grok (default, native audio)
460156
+ $ vibe generate video "dancing cat" -o cat.mp4 # Seedance when FAL_KEY is set
460157
+ $ vibe generate video "city timelapse" -o city.mp4 -p seedance # Seedance via fal.ai
459424
460158
  $ vibe generate video "city timelapse" -o city.mp4 -p kling # Kling
459425
460159
  $ vibe generate video "epic scene" -i frame.png -o out.mp4 -p runway # Image-to-video
459426
460160
  $ vibe generate speech "Hello world" -o hello.mp3
@@ -459430,7 +460164,8 @@ Examples:
459430
460164
  API Keys (per provider):
459431
460165
  GOOGLE_API_KEY Image (default), Veo video
459432
460166
  OPENAI_API_KEY Image (-p openai)
459433
- XAI_API_KEY Grok image/video (default video)
460167
+ FAL_KEY Seedance video (-p seedance, default video)
460168
+ XAI_API_KEY Grok image/video
459434
460169
  KLING_API_KEY Kling video (-p kling)
459435
460170
  RUNWAY_API_SECRET Runway video (-p runway)
459436
460171
  ELEVENLABS_API_KEY Speech, sound effects, music
@@ -459478,12 +460213,20 @@ async function executeVideoGenerate(options) {
459478
460213
  negative,
459479
460214
  resolution,
459480
460215
  veoModel = "3.1-fast",
460216
+ seedanceModel = "quality",
459481
460217
  output: output3,
459482
460218
  wait = true,
459483
460219
  apiKey
459484
460220
  } = options;
459485
460221
  try {
459486
- const envKeyMap = { grok: "XAI_API_KEY", runway: "RUNWAY_API_SECRET", kling: "KLING_API_KEY", veo: "GOOGLE_API_KEY" };
460222
+ const envKeyMap = {
460223
+ grok: "XAI_API_KEY",
460224
+ runway: "RUNWAY_API_SECRET",
460225
+ kling: "KLING_API_KEY",
460226
+ veo: "GOOGLE_API_KEY",
460227
+ seedance: "FAL_KEY",
460228
+ fal: "FAL_KEY"
460229
+ };
459487
460230
  const key2 = apiKey || process.env[envKeyMap[provider] || ""];
459488
460231
  if (!key2) return { success: false, error: `${envKeyMap[provider]} required for ${provider}` };
459489
460232
  let referenceImage;
@@ -459495,7 +460238,36 @@ async function executeVideoGenerate(options) {
459495
460238
  const mimeType = mimeTypes[ext || "png"] || "image/png";
459496
460239
  referenceImage = `data:${mimeType};base64,${imageBuffer.toString("base64")}`;
459497
460240
  }
459498
- if (provider === "runway") {
460241
+ if (provider === "seedance" || provider === "fal") {
460242
+ const fal = new FalProvider();
460243
+ await fal.initialize({ apiKey: key2 });
460244
+ let falImage = referenceImage;
460245
+ if (falImage && falImage.startsWith("data:")) {
460246
+ const imgbbKey = process.env.IMGBB_API_KEY;
460247
+ if (!imgbbKey) return { success: false, error: "IMGBB_API_KEY required for Seedance image-to-video" };
460248
+ const base64Data = falImage.split(",")[1];
460249
+ const uploadResult = await uploadToImgbb(Buffer.from(base64Data, "base64"), imgbbKey);
460250
+ if (!uploadResult.success || !uploadResult.url) return { success: false, error: `ImgBB upload failed: ${uploadResult.error}` };
460251
+ falImage = uploadResult.url;
460252
+ }
460253
+ const model = seedanceModel === "fast" || seedanceModel === "seedance-2.0-fast" ? "seedance-2.0-fast" : "seedance-2.0";
460254
+ const result = await fal.generateVideo(prompt3, {
460255
+ prompt: prompt3,
460256
+ referenceImage: falImage,
460257
+ duration,
460258
+ aspectRatio: ratio,
460259
+ negativePrompt: negative,
460260
+ model
460261
+ });
460262
+ if (result.status === "failed") return { success: false, error: result.error || "Seedance generation failed" };
460263
+ let outputPath;
460264
+ if (output3 && result.videoUrl) {
460265
+ const buffer = await downloadVideo(result.videoUrl, key2);
460266
+ outputPath = resolve50(process.cwd(), output3);
460267
+ await writeFile34(outputPath, buffer);
460268
+ }
460269
+ return { success: true, taskId: result.id, status: "completed", videoUrl: result.videoUrl, outputPath, provider: "seedance" };
460270
+ } else if (provider === "runway") {
459499
460271
  const runway = new RunwayProvider();
459500
460272
  await runway.initialize({ apiKey: key2 });
459501
460273
  const result = await runway.generateVideo(prompt3, {
@@ -459892,20 +460664,24 @@ var init_detect = __esm({
459892
460664
  init_validate();
459893
460665
  detectCommand = new Command("detect").description("Auto-detect scenes, beats, and silences in media");
459894
460666
  detectCommand.command("scenes").description("Detect scene changes in video").argument("<video>", "Video file path").option("-t, --threshold <value>", "Scene change threshold (0-1)", "0.3").option("-o, --output <path>", "Output JSON file with timestamps").option("-p, --project <path>", "Add scenes as clips to project").option("--dry-run", "Preview parameters without executing").action(async (videoPath, options) => {
460667
+ const startedAt = Date.now();
459895
460668
  const spinner2 = ora("Detecting scenes...").start();
459896
460669
  try {
459897
460670
  if (options.output) {
459898
460671
  validateOutputPath(options.output);
459899
460672
  }
459900
460673
  if (options.dryRun) {
459901
- outputResult({
459902
- dryRun: true,
460674
+ outputSuccess({
459903
460675
  command: "detect scenes",
459904
- params: {
459905
- video: videoPath,
459906
- threshold: options.threshold,
459907
- output: options.output || null,
459908
- project: options.project || null
460676
+ startedAt,
460677
+ dryRun: true,
460678
+ data: {
460679
+ params: {
460680
+ video: videoPath,
460681
+ threshold: options.threshold,
460682
+ output: options.output || null,
460683
+ project: options.project || null
460684
+ }
459909
460685
  }
459910
460686
  });
459911
460687
  return;
@@ -460007,20 +460783,24 @@ var init_detect = __esm({
460007
460783
  }
460008
460784
  });
460009
460785
  detectCommand.command("silence").description("Detect silence in audio/video").argument("<media>", "Media file path").option("-n, --noise <dB>", "Noise threshold in dB", "-30").option("-d, --duration <sec>", "Minimum silence duration", "0.5").option("-o, --output <path>", "Output JSON file with timestamps").option("--dry-run", "Preview parameters without executing").action(async (mediaPath, options) => {
460786
+ const startedAt = Date.now();
460010
460787
  const spinner2 = ora("Detecting silence...").start();
460011
460788
  try {
460012
460789
  if (options.output) {
460013
460790
  validateOutputPath(options.output);
460014
460791
  }
460015
460792
  if (options.dryRun) {
460016
- outputResult({
460017
- dryRun: true,
460793
+ outputSuccess({
460018
460794
  command: "detect silence",
460019
- params: {
460020
- media: mediaPath,
460021
- noise: options.noise,
460022
- duration: options.duration,
460023
- output: options.output || null
460795
+ startedAt,
460796
+ dryRun: true,
460797
+ data: {
460798
+ params: {
460799
+ media: mediaPath,
460800
+ noise: options.noise,
460801
+ duration: options.duration,
460802
+ output: options.output || null
460803
+ }
460024
460804
  }
460025
460805
  });
460026
460806
  return;
@@ -460089,18 +460869,22 @@ var init_detect = __esm({
460089
460869
  }
460090
460870
  });
460091
460871
  detectCommand.command("beats").description("Detect beats in audio (for music sync)").argument("<audio>", "Audio file path").option("-o, --output <path>", "Output JSON file with timestamps").option("--dry-run", "Preview parameters without executing").action(async (audioPath, options) => {
460872
+ const startedAt = Date.now();
460092
460873
  const spinner2 = ora("Detecting beats...").start();
460093
460874
  try {
460094
460875
  if (options.output) {
460095
460876
  validateOutputPath(options.output);
460096
460877
  }
460097
460878
  if (options.dryRun) {
460098
- outputResult({
460099
- dryRun: true,
460879
+ outputSuccess({
460100
460880
  command: "detect beats",
460101
- params: {
460102
- audio: audioPath,
460103
- output: options.output || null
460881
+ startedAt,
460882
+ dryRun: true,
460883
+ data: {
460884
+ params: {
460885
+ audio: audioPath,
460886
+ output: options.output || null
460887
+ }
460104
460888
  }
460105
460889
  });
460106
460890
  return;
@@ -460698,334 +461482,8 @@ function visualStyleNames() {
460698
461482
  return STYLES.map((s) => `"${s.name}"`).join(", ");
460699
461483
  }
460700
461484
 
460701
- // ../cli/src/commands/_shared/scene-project.ts
460702
- var import_yaml = __toESM(require_dist(), 1);
460703
- import { mkdir, readFile, writeFile, access } from "node:fs/promises";
460704
- import { resolve, basename } from "node:path";
460705
- var ASPECT_DIMS = {
460706
- "16:9": { width: 1920, height: 1080 },
460707
- "9:16": { width: 1080, height: 1920 },
460708
- "1:1": { width: 1080, height: 1080 },
460709
- "4:5": { width: 1080, height: 1350 }
460710
- };
460711
- function aspectToDims(aspect) {
460712
- return ASPECT_DIMS[aspect];
460713
- }
460714
- function defaultVibeProjectConfig(name) {
460715
- return {
460716
- name,
460717
- aspect: "16:9",
460718
- defaultSceneDuration: 5,
460719
- providers: { image: null, tts: null, transcribe: null },
460720
- budget: { maxUsd: 0 }
460721
- };
460722
- }
460723
- function buildHyperframesConfig() {
460724
- return {
460725
- $schema: "https://hyperframes.heygen.com/schema/hyperframes.json",
460726
- registry: "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
460727
- paths: {
460728
- blocks: "compositions",
460729
- components: "compositions/components",
460730
- assets: "assets"
460731
- }
460732
- };
460733
- }
460734
- function buildHyperframesMeta(name, now = /* @__PURE__ */ new Date()) {
460735
- return { id: name, name, createdAt: now.toISOString() };
460736
- }
460737
- function mergeHyperframesConfig(existing, defaults) {
460738
- const out = { ...defaults, ...existing };
460739
- if (existing.paths || defaults.paths) {
460740
- out.paths = { ...defaults.paths ?? {}, ...existing.paths ?? {} };
460741
- }
460742
- return out;
460743
- }
460744
- function buildEmptyRootHtml(opts) {
460745
- const { width, height } = ASPECT_DIMS[opts.aspect];
460746
- return `<!doctype html>
460747
- <html lang="en">
460748
- <head>
460749
- <meta charset="UTF-8" />
460750
- <meta name="viewport" content="width=${width}, height=${height}" />
460751
- <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
460752
- <style>
460753
- * { margin: 0; padding: 0; box-sizing: border-box; }
460754
- html, body {
460755
- margin: 0;
460756
- width: ${width}px;
460757
- height: ${height}px;
460758
- overflow: hidden;
460759
- background: #000;
460760
- }
460761
- </style>
460762
- </head>
460763
- <body>
460764
- <div
460765
- id="root"
460766
- data-composition-id="main"
460767
- data-start="0"
460768
- data-duration="${opts.duration}"
460769
- data-width="${width}"
460770
- data-height="${height}"
460771
- >
460772
- <!-- Scenes added via \`vibe scene add\` are inserted here. -->
460773
- <!-- Each scene reference: data-composition-id, data-composition-src, data-start, data-duration, data-track-index. -->
460774
- <!-- See compositions/*.html for sub-composition contents. -->
460775
-
460776
- </div>
460777
-
460778
- <script>
460779
- window.__timelines = window.__timelines || {};
460780
- window.__timelines["main"] = gsap.timeline({ paused: true });
460781
- </script>
460782
- </body>
460783
- </html>
460784
- `;
460785
- }
460786
- function buildDesignMd(opts) {
460787
- const { name, style } = opts;
460788
- const intro = style ? `Visual identity for **${name}**, scaffolded from the **${style.name}** style (after ${style.designer}). Customise freely \u2014 this file is the single source of truth for every scene's palette, typography, and motion.` : `Visual identity for **${name}**. Fill the sections below before authoring any scene HTML or generating any backdrop. Pick a named style with \`vibe scene styles\` if you want a credible starting point.`;
460789
- const moodLine = style ? `**Mood:** ${style.mood} \xB7 **Best for:** ${style.bestFor}` : `**Mood:** _(one line \u2014 what should the viewer FEEL?)_`;
460790
- const palette = style ? `${style.palette.map((c) => `- \`${c}\``).join("\n")}
460791
-
460792
- ${style.paletteNotes}` : `- _hex_ \u2014 primary
460793
- - _hex_ \u2014 accent
460794
-
460795
- _2\u20133 colours max. Declare explicit hex values; never name colours abstractly._`;
460796
- const typography = style ? style.typography : `_One family, two weights. State the role of each (headline / label / body)._`;
460797
- const composition = style ? style.composition : `_Grid? Centered? Layered? How does negative space behave?_`;
460798
- const motion = style ? `${style.motion}
460799
-
460800
- **GSAP signature:** ${style.gsapSignature}` : `_How fast? Snappy or fluid? Overshoot or precision?_
460801
-
460802
- **GSAP signature:** _e.g. \`expo.out\`, \`sine.inOut\`, \`back.out(1.8)\`_`;
460803
- const transition = style ? style.transition : `_Which Hyperframes shader matches the energy? (Cinematic Zoom, Cross-Warp Morph, Glitch, Domain Warp, \u2026)_`;
460804
- const avoid = style ? style.avoid.map((a) => `- ${a}`).join("\n") : `- _anti-pattern 1_
460805
- - _anti-pattern 2_
460806
- - _anti-pattern 3_`;
460807
- return `# ${name} \u2014 Design
460808
-
460809
- > **Hard-gate.** This file defines the visual identity of every scene.
460810
- > Author it before generating any HTML, backdrop image, or motion.
460811
- > The Hyperframes \`hyperframes\` skill enforces this: scenes that
460812
- > contradict DESIGN.md are rejected.
460813
-
460814
- ${intro}
460815
-
460816
- ## Style
460817
-
460818
- ${moodLine}
460819
-
460820
- ## Palette
460821
-
460822
- ${palette}
460823
-
460824
- ## Typography
460825
-
460826
- ${typography}
460827
-
460828
- ## Composition
460829
-
460830
- ${composition}
460831
-
460832
- ## Motion
460833
-
460834
- ${motion}
460835
-
460836
- ## Transition
460837
-
460838
- ${transition}
460839
-
460840
- ## What NOT to do
460841
-
460842
- ${avoid}
460843
-
460844
- ---
460845
-
460846
- _Browse other named styles: \`vibe scene styles\`_
460847
- ${style ? `_This file was seeded by \`vibe scene init --visual-style "${style.name}"\`._` : `_Seed this file from a named style: \`vibe scene init <dir> --visual-style "<name>"\`._`}
460848
- `;
460849
- }
460850
- function buildProjectClaudeMd(name) {
460851
- return `# ${name} \u2014 Scene Authoring Project
460852
-
460853
- This project is **bilingual**: it works with both VibeFrame (\`vibe\`) and
460854
- HeyGen Hyperframes (\`hyperframes\`). You can run either CLI inside this
460855
- directory.
460856
-
460857
- ## Visual identity hard-gate
460858
-
460859
- **Author \`DESIGN.md\` before any scene HTML.** It defines palette,
460860
- typography, motion, and transition rules. Both the agent-driven path and
460861
- the fallback emit reference it; scenes that contradict DESIGN.md are
460862
- rejected by the Hyperframes \`hyperframes\` skill.
460863
-
460864
- Browse named styles: \`vibe scene styles\`. Re-seed from one with
460865
- \`vibe scene init . --visual-style "Swiss Pulse"\` (idempotent).
460866
-
460867
- ## Skills \u2014 USE THESE FIRST
460868
-
460869
- **Always invoke the relevant skill before authoring scenes.** Skills encode
460870
- framework-specific patterns (GSAP timeline registration, data-attribute
460871
- semantics, VibeFrame pipeline conventions) that are NOT in generic web docs.
460872
-
460873
- | Skill | Command | When to use |
460874
- | ----------------- | ---------------- | ------------------------------------------------------------------------------------- |
460875
- | **hyperframes** | \`/hyperframes\` | Cinematic-quality composition \u2014 DESIGN.md hard-gate, named styles, motion principles |
460876
- | **vibe-scene** | \`/vibe-scene\` | VibeFrame's authoring loop, AI assets, lint feedback, pipeline integration |
460877
- | **gsap** | \`/gsap\` | GSAP tweens, timelines, easing |
460878
-
460879
- Install the Hyperframes skills once per machine:
460880
-
460881
- \`\`\`bash
460882
- npx skills add heygen-com/hyperframes
460883
- \`\`\`
460884
-
460885
- Restart your agent session (or reload the skill list) after installing.
460886
- If skills aren't available, follow the **Key Rules** below \u2014 they cover
460887
- the framework-level minimum, not the cinematic craft layer.
460888
-
460889
- ## Project structure
460890
-
460891
- - \`DESIGN.md\` \u2014 visual identity contract (palette, type, motion, transitions)
460892
- - \`index.html\` \u2014 root composition (timeline)
460893
- - \`compositions/scene-*.html\` \u2014 per-scene HTML authored by you or the agent
460894
- - \`assets/\` \u2014 shared media (narration audio, images, video)
460895
- - \`transcript.json\` \u2014 Whisper word-level transcript (if narration exists)
460896
- - \`hyperframes.json\` \u2014 HF registry config (speak to both toolchains)
460897
- - \`vibe.project.yaml\` \u2014 VibeFrame config (providers, budget)
460898
- - \`renders/\` \u2014 output MP4s
460899
-
460900
- ## Commands
460901
-
460902
- \`\`\`bash
460903
- vibe scene add <name> --narration "..." --visuals "..." # Author a new scene via AI
460904
- vibe scene lint # Validate scenes (in-process HF linter)
460905
- vibe scene render # Render to MP4
460906
-
460907
- # Hyperframes CLI (if installed \u2014 works in this project too)
460908
- npx hyperframes preview
460909
- npx hyperframes render
460910
- \`\`\`
460911
-
460912
- ## Key Rules (for hand-authored scene HTML)
460913
-
460914
- 1. Every timed element needs \`data-start\`, \`data-duration\`, and \`data-track-index\`.
460915
- 2. Elements with timing **MUST** have \`class="clip"\` \u2014 the framework uses this for visibility control.
460916
- 3. Timelines must be paused and registered on \`window.__timelines\`:
460917
- \`\`\`js
460918
- window.__timelines = window.__timelines || {};
460919
- window.__timelines["composition-id"] = gsap.timeline({ paused: true });
460920
- \`\`\`
460921
- 4. Videos use \`muted\` with a separate \`<audio>\` element for the audio track.
460922
- 5. Sub-compositions use \`data-composition-src="compositions/file.html"\`.
460923
- 6. Only deterministic logic \u2014 no \`Date.now()\`, \`Math.random()\`, or network fetches.
460924
-
460925
- ## Linting \u2014 run after changes
460926
-
460927
- \`\`\`bash
460928
- vibe scene lint # preferred \u2014 in-process, no network
460929
- vibe scene lint --fix # auto-fix mechanical issues
460930
- vibe scene lint --json # structured output for agent loops
460931
- \`\`\`
460932
- `;
460933
- }
460934
- function buildSceneGitignore() {
460935
- return `# VibeFrame caches
460936
- .vibeframe/cache/
460937
- .vibeframe/checkpoints/
460938
-
460939
- # Render outputs
460940
- renders/*.mp4
460941
- tmp/
460942
-
460943
- # OS / editor
460944
- .DS_Store
460945
- *.log
460946
- `;
460947
- }
460948
- async function pathExists(p) {
460949
- try {
460950
- await access(p);
460951
- return true;
460952
- } catch {
460953
- return false;
460954
- }
460955
- }
460956
- async function scaffoldSceneProject(opts) {
460957
- const dir = resolve(opts.dir);
460958
- const name = opts.name ?? basename(dir);
460959
- const aspect = opts.aspect ?? "16:9";
460960
- const duration = opts.duration ?? 10;
460961
- const now = opts.now ?? /* @__PURE__ */ new Date();
460962
- await mkdir(dir, { recursive: true });
460963
- await mkdir(resolve(dir, "compositions"), { recursive: true });
460964
- await mkdir(resolve(dir, "assets"), { recursive: true });
460965
- const created = [];
460966
- const skipped2 = [];
460967
- const merged = [];
460968
- const hfPath = resolve(dir, "hyperframes.json");
460969
- const hfDefaults = buildHyperframesConfig();
460970
- if (await pathExists(hfPath)) {
460971
- const existingRaw = await readFile(hfPath, "utf-8");
460972
- const existing = JSON.parse(existingRaw);
460973
- const mergedConfig = mergeHyperframesConfig(existing, hfDefaults);
460974
- await writeFile(hfPath, JSON.stringify(mergedConfig, null, 2) + "\n", "utf-8");
460975
- merged.push(hfPath);
460976
- } else {
460977
- await writeFile(hfPath, JSON.stringify(hfDefaults, null, 2) + "\n", "utf-8");
460978
- created.push(hfPath);
460979
- }
460980
- const metaPath = resolve(dir, "meta.json");
460981
- if (await pathExists(metaPath)) {
460982
- skipped2.push(metaPath);
460983
- } else {
460984
- await writeFile(metaPath, JSON.stringify(buildHyperframesMeta(name, now), null, 2) + "\n", "utf-8");
460985
- created.push(metaPath);
460986
- }
460987
- const rootPath = resolve(dir, "index.html");
460988
- if (await pathExists(rootPath)) {
460989
- skipped2.push(rootPath);
460990
- } else {
460991
- await writeFile(rootPath, buildEmptyRootHtml({ aspect, duration }), "utf-8");
460992
- created.push(rootPath);
460993
- }
460994
- const vibePath = resolve(dir, "vibe.project.yaml");
460995
- if (await pathExists(vibePath)) {
460996
- skipped2.push(vibePath);
460997
- } else {
460998
- const cfg = { ...defaultVibeProjectConfig(name), aspect };
460999
- await writeFile(vibePath, (0, import_yaml.stringify)(cfg), "utf-8");
461000
- created.push(vibePath);
461001
- }
461002
- const claudePath = resolve(dir, "CLAUDE.md");
461003
- if (await pathExists(claudePath)) {
461004
- skipped2.push(claudePath);
461005
- } else {
461006
- await writeFile(claudePath, buildProjectClaudeMd(name), "utf-8");
461007
- created.push(claudePath);
461008
- }
461009
- const designPath = resolve(dir, "DESIGN.md");
461010
- if (await pathExists(designPath)) {
461011
- skipped2.push(designPath);
461012
- } else {
461013
- await writeFile(
461014
- designPath,
461015
- buildDesignMd({ name, style: opts.visualStyle }),
461016
- "utf-8"
461017
- );
461018
- created.push(designPath);
461019
- }
461020
- const gitignorePath = resolve(dir, ".gitignore");
461021
- if (await pathExists(gitignorePath)) {
461022
- skipped2.push(gitignorePath);
461023
- } else {
461024
- await writeFile(gitignorePath, buildSceneGitignore(), "utf-8");
461025
- created.push(gitignorePath);
461026
- }
461027
- return { created, skipped: skipped2, merged };
461028
- }
461485
+ // ../cli/src/tools/manifest/scene.ts
461486
+ init_scene_project();
461029
461487
 
461030
461488
  // ../cli/src/commands/scene.ts
461031
461489
  init_esm();
@@ -461034,6 +461492,7 @@ init_ora();
461034
461492
  var import_yaml5 = __toESM(require_dist(), 1);
461035
461493
  init_dist();
461036
461494
  init_tts_resolve();
461495
+ init_scene_project();
461037
461496
  import { basename as basename6, resolve as resolve21, relative as relative7, dirname as dirname17 } from "node:path";
461038
461497
  import { mkdir as mkdir10, readFile as readFile10, writeFile as writeFile10, access as access4, copyFile as copyFile2 } from "node:fs/promises";
461039
461498
  import { existsSync as existsSync28 } from "node:fs";
@@ -461525,6 +461984,7 @@ function deriveInstallHosts(detected) {
461525
461984
  // ../cli/src/commands/scene.ts
461526
461985
  init_compose_prompts();
461527
461986
  var VALID_ASPECTS2 = ["16:9", "9:16", "1:1", "4:5"];
461987
+ var VALID_SCENE_INIT_PROFILES = ["minimal", "agent", "full"];
461528
461988
  function validateAspect(value) {
461529
461989
  if (!VALID_ASPECTS2.includes(value)) {
461530
461990
  exitWithError(usageError(`Invalid aspect ratio: ${value}`, `Valid: ${VALID_ASPECTS2.join(", ")}`));
@@ -461556,7 +462016,12 @@ function validateVisualStyle(value) {
461556
462016
  }
461557
462017
  return found;
461558
462018
  }
461559
- var sceneCommand = new Command("scene").description("Author and render per-scene HTML compositions (Hyperframes backend)").addHelpText("after", `
462019
+ function formatSceneInitProfile(profile) {
462020
+ if (profile === "minimal") return "authoring files only; build will add render scaffold when needed";
462021
+ if (profile === "agent") return "authoring files plus local composition rules for host agents";
462022
+ return "complete authoring, agent, and render scaffold";
462023
+ }
462024
+ var sceneCommand = new Command("scene").description("Advanced scene commands for VibeFrame video projects").addHelpText("after", `
461560
462025
  Examples:
461561
462026
  $ vibe scene init my-video # Scaffold a new project
461562
462027
  $ vibe scene init my-video -r 9:16 -d 30 # Vertical 30s project
@@ -461564,62 +462029,91 @@ Examples:
461564
462029
  --headline "Welcome to VibeFrame" # Headline-only scene
461565
462030
  $ vibe scene add overview --narration "VibeFrame turns scripts into video." \\
461566
462031
  --visuals "studio desk, soft lighting" # AI narration + image
461567
- $ vibe scene lint # Validate every scene against Hyperframes rules
462032
+ $ vibe scene lint # Validate every scene against composition rules
461568
462033
  $ vibe scene lint --fix # Auto-fix mechanical issues (e.g. missing class="clip")
461569
462034
  $ vibe scene lint --json # Structured output for agent loops
461570
462035
  $ vibe scene render # Render to renders/<name>-<timestamp>.mp4
461571
462036
  $ vibe scene render -o demo.mp4 --quality high # Custom output path + quality
461572
462037
  $ vibe scene render --fps 60 --format webm # 60fps WebM render
461573
462038
 
461574
- A scene project is bilingual: it works with both \`vibe\` and \`npx hyperframes\`.
462039
+ Most users can start with \`vibe init\`, \`vibe build\`, and \`vibe render\`.
462040
+ This namespace exposes lower-level scene authoring and rendering controls.
461575
462041
  Run 'vibe schema scene.<command>' for structured parameter info.`);
461576
- sceneCommand.command("init").description("Scaffold a new scene project (or safely augment an existing Hyperframes project)").argument("<dir>", "Project directory (created if it doesn't exist)").option("-n, --name <name>", "Project name (defaults to directory basename)").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1, 4:5", "16:9").option("-d, --duration <sec>", "Default root composition duration (seconds)", "10").option("--visual-style <name>", `Seed DESIGN.md from a named style (browse via \`vibe scene styles\`). E.g. "Swiss Pulse"`).option("--dry-run", "Preview parameters without writing files").action(async (dir, options) => {
462042
+ sceneCommand.command("init").description("Scaffold a new scene project (or safely augment an existing project)").argument("<dir>", "Project directory (created if it doesn't exist)").option("-n, --name <name>", "Project name (defaults to directory basename)").option("-r, --ratio <ratio>", "Aspect ratio: 16:9, 9:16, 1:1, 4:5", "16:9").option("-d, --duration <sec>", "Default root composition duration (seconds)", "10").option("--visual-style <name>", `Seed DESIGN.md from a named style (browse via \`vibe scene styles\`). E.g. "Swiss Pulse"`).option("--profile <profile>", "Scene profile: minimal (storyboard/design only), agent (recommended), full (render scaffold upfront)", "agent").option("--dry-run", "Preview parameters without writing files").action(async (dir, options) => {
462043
+ const startedAt = Date.now();
461577
462044
  const aspect = validateAspect(options.ratio);
461578
462045
  const duration = validateDuration(options.duration);
461579
462046
  const name = options.name ?? basename6(dir.replace(/\/+$/, ""));
461580
462047
  const visualStyle = options.visualStyle ? validateVisualStyle(options.visualStyle) : void 0;
462048
+ const profile = String(options.profile ?? "agent");
462049
+ if (!isSceneScaffoldProfile(profile)) {
462050
+ exitWithError(usageError(`Invalid --profile: ${profile}`, `Must be one of: ${VALID_SCENE_INIT_PROFILES.join(", ")}`));
462051
+ }
462052
+ const groups = describeSceneScaffold({ dir, profile });
461581
462053
  if (options.dryRun) {
461582
- outputResult({
461583
- dryRun: true,
462054
+ if (!isJsonMode() && !isQuietMode()) {
462055
+ printSceneInitDryRun({ dir, name, aspect, duration, visualStyleName: visualStyle?.name ?? null, profile, groups });
462056
+ return;
462057
+ }
462058
+ outputSuccess({
461584
462059
  command: "scene init",
461585
- params: {
461586
- dir,
461587
- name,
461588
- aspect,
461589
- duration,
461590
- visualStyle: visualStyle?.name ?? null
462060
+ startedAt,
462061
+ dryRun: true,
462062
+ data: {
462063
+ params: {
462064
+ dir,
462065
+ name,
462066
+ aspect,
462067
+ duration,
462068
+ visualStyle: visualStyle?.name ?? null,
462069
+ profile
462070
+ },
462071
+ groups
461591
462072
  }
461592
462073
  });
461593
462074
  return;
461594
462075
  }
461595
- const spinner2 = isJsonMode() ? null : ora(`Scaffolding scene project at ${dir}...`).start();
462076
+ const spinner2 = isJsonMode() || isQuietMode() ? null : ora(`Scaffolding scene project at ${dir}...`).start();
461596
462077
  try {
461597
- const result = await scaffoldSceneProject({ dir, name, aspect, duration, visualStyle });
462078
+ const result = await scaffoldSceneProject({ dir, name, aspect, duration, visualStyle, profile });
461598
462079
  const detectedIds = detectedAgentHosts().map((h) => h.id);
461599
462080
  const skillHosts = deriveInstallHosts(detectedIds);
461600
462081
  const projectAbs = resolve21(dir);
461601
- const skillResult = await installHyperframesSkill({
462082
+ const skillResult = profile === "agent" || profile === "full" ? await installHyperframesSkill({
461602
462083
  projectDir: projectAbs,
461603
462084
  hosts: skillHosts
461604
- });
461605
- if (isJsonMode()) {
461606
- outputResult({
461607
- success: true,
462085
+ }) : { success: true, files: [], bundleVersion: "not-installed" };
462086
+ if (isJsonMode() || isQuietMode()) {
462087
+ outputSuccess({
461608
462088
  command: "scene init",
461609
- dir,
461610
- name,
461611
- aspect,
461612
- duration,
461613
- visualStyle: visualStyle?.name ?? null,
461614
- created: result.created,
461615
- merged: result.merged,
461616
- skipped: result.skipped,
461617
- skillFiles: skillResult.files,
461618
- skillBundleVersion: skillResult.bundleVersion
462089
+ startedAt,
462090
+ data: {
462091
+ dir,
462092
+ name,
462093
+ aspect,
462094
+ duration,
462095
+ visualStyle: visualStyle?.name ?? null,
462096
+ profile,
462097
+ created: result.created,
462098
+ merged: result.merged,
462099
+ skipped: result.skipped,
462100
+ groups: result.groups,
462101
+ skillFiles: skillResult.files,
462102
+ skillBundleVersion: skillResult.bundleVersion
462103
+ }
461619
462104
  });
461620
462105
  return;
461621
462106
  }
461622
- spinner2?.succeed(source_default.green(`Scene project ready: ${dir}`));
462107
+ spinner2?.succeed(source_default.green(`Video project ready: ${dir}`));
462108
+ console.log();
462109
+ console.log(source_default.bold.cyan("Edit first"));
462110
+ console.log(source_default.dim("\u2500".repeat(60)));
462111
+ console.log(` ${source_default.bold("STORYBOARD.md")} ${source_default.dim("# beats: narration, backdrop, minimum duration")}`);
462112
+ console.log(` ${source_default.bold("DESIGN.md")} ${source_default.dim("# palette, typography, motion rules")}`);
462113
+ console.log();
462114
+ console.log(source_default.bold.cyan("Profile"));
462115
+ console.log(source_default.dim("\u2500".repeat(60)));
462116
+ console.log(` ${source_default.bold(profile)} ${source_default.dim(formatSceneInitProfile(profile))}`);
461623
462117
  console.log();
461624
462118
  console.log(source_default.bold.cyan("Files"));
461625
462119
  console.log(source_default.dim("\u2500".repeat(60)));
@@ -461630,7 +462124,7 @@ sceneCommand.command("init").description("Scaffold a new scene project (or safel
461630
462124
  const skillSkipped = skillResult.files.filter((f) => f.status === "skipped-exists");
461631
462125
  if (skillWritten.length + skillSkipped.length > 0) {
461632
462126
  console.log();
461633
- console.log(source_default.bold.cyan("Hyperframes skill"));
462127
+ console.log(source_default.bold.cyan("Composition rules"));
461634
462128
  console.log(source_default.dim("\u2500".repeat(60)));
461635
462129
  for (const f of skillWritten) console.log(source_default.green(" +"), f.path);
461636
462130
  for (const f of skillSkipped) console.log(source_default.dim(" \xB7"), f.path, source_default.dim("(kept existing)"));
@@ -461644,18 +462138,42 @@ sceneCommand.command("init").description("Scaffold a new scene project (or safel
461644
462138
  } else {
461645
462139
  console.log(` ${source_default.cyan("vibe scene styles")} ${source_default.dim("# pick a named style for DESIGN.md")}`);
461646
462140
  }
461647
- console.log(` ${source_default.dim("Your agent now has Hyperframes rules in")} ${source_default.cyan("SKILL.md")} ${source_default.dim("\u2014 ask it to author scene HTML directly.")}`);
462141
+ if (profile === "agent" || profile === "full") {
462142
+ console.log(` ${source_default.dim("Your agent now has composition rules in")} ${source_default.cyan("SKILL.md")} ${source_default.dim("\u2014 ask it to author scene HTML directly.")}`);
462143
+ } else {
462144
+ console.log(` ${source_default.cyan("vibe scene install-skill")} ${source_default.dim("# add agent authoring rules later")}`);
462145
+ }
461648
462146
  console.log(` ${source_default.cyan("vibe scene add")} <name> ${source_default.dim("# fallback: 5-preset emit (no agent)")}`);
461649
- console.log(` ${source_default.cyan("vibe scene lint")} ${source_default.dim("# validate HTML")}`);
461650
- console.log(` ${source_default.cyan("vibe scene render")} ${source_default.dim("# render to MP4")}`);
462147
+ console.log(` ${source_default.cyan("vibe build")} ${source_default.dim("# build STORYBOARD.md into scenes/assets")}`);
462148
+ console.log(` ${source_default.cyan("vibe render")} ${source_default.dim("# render to video")}`);
461651
462149
  } catch (error) {
461652
462150
  spinner2?.fail("Failed to scaffold scene project");
461653
462151
  const msg = error instanceof Error ? error.message : String(error);
461654
462152
  exitWithError(generalError(`Failed to scaffold: ${msg}`));
461655
462153
  }
461656
462154
  });
462155
+ function printSceneInitDryRun(opts) {
462156
+ console.log();
462157
+ console.log(source_default.bold.cyan("VibeFrame Scene Init - dry run"));
462158
+ console.log(source_default.dim("-".repeat(60)));
462159
+ console.log(` Project: ${source_default.bold(opts.dir)}`);
462160
+ console.log(` Name: ${source_default.bold(opts.name)}`);
462161
+ console.log(` Profile: ${source_default.bold(opts.profile)} ${source_default.dim(formatSceneInitProfile(opts.profile))}`);
462162
+ console.log(` Aspect: ${opts.aspect}`);
462163
+ console.log(` Duration: ${opts.duration}s`);
462164
+ console.log(` Visual style: ${opts.visualStyleName ?? "none"}`);
462165
+ console.log();
462166
+ console.log(source_default.bold.cyan("Files that would be prepared"));
462167
+ console.log(source_default.dim("-".repeat(60)));
462168
+ for (const file of opts.groups.authoring) console.log(` authoring ${file}`);
462169
+ for (const file of opts.groups.agent) console.log(` agent ${file}`);
462170
+ for (const file of opts.groups.render) console.log(` render ${file}`);
462171
+ console.log();
462172
+ console.log(source_default.dim("No files were written."));
462173
+ }
461657
462174
  var VALID_INSTALL_SKILL_HOSTS = ["claude-code", "cursor", "auto", "all"];
461658
462175
  sceneCommand.command("install-skill").description("Install the Hyperframes skill into a scene project so the host agent can read it (Phase H1)").argument("[project-dir]", "Project directory containing STORYBOARD.md / DESIGN.md", ".").option("--host <id>", `Host layout target: ${VALID_INSTALL_SKILL_HOSTS.join(" | ")}`, "auto").option("--force", "Overwrite existing skill files (default: skip-on-exist)").option("--dry-run", "Preview which files would be written without changing anything").action(async (projectDirArg, options) => {
462176
+ const startedAt = Date.now();
461659
462177
  const hostFlag = options.host ?? "auto";
461660
462178
  if (!VALID_INSTALL_SKILL_HOSTS.includes(hostFlag)) {
461661
462179
  exitWithError(usageError(`Invalid --host: ${hostFlag}`, `Valid: ${VALID_INSTALL_SKILL_HOSTS.join(", ")}`));
@@ -461675,15 +462193,17 @@ sceneCommand.command("install-skill").description("Install the Hyperframes skill
461675
462193
  dryRun: options.dryRun ?? false
461676
462194
  });
461677
462195
  if (isJsonMode()) {
461678
- outputResult({
461679
- success: true,
462196
+ outputSuccess({
461680
462197
  command: "scene install-skill",
461681
- projectDir,
461682
- host: hostFlag,
461683
- resolvedHosts: hosts,
461684
- bundleVersion: result.bundleVersion,
461685
- files: result.files,
461686
- dryRun: options.dryRun ?? false
462198
+ startedAt,
462199
+ dryRun: options.dryRun ?? false,
462200
+ data: {
462201
+ projectDir,
462202
+ host: hostFlag,
462203
+ resolvedHosts: hosts,
462204
+ bundleVersion: result.bundleVersion,
462205
+ files: result.files
462206
+ }
461687
462207
  });
461688
462208
  return;
461689
462209
  }
@@ -461705,6 +462225,7 @@ sceneCommand.command("install-skill").description("Install the Hyperframes skill
461705
462225
  }
461706
462226
  });
461707
462227
  sceneCommand.command("compose-prompts").description("Emit the per-beat compose plan for the host agent to author HTML itself (Phase H2 \u2014 no LLM call)").argument("[project-dir]", "Project directory containing STORYBOARD.md / DESIGN.md", ".").option("--beat <id>", "Restrict the plan to a single beat by id (e.g. 'hook', '1')").action(async (projectDirArg, options) => {
462228
+ const startedAt = Date.now();
461708
462229
  const projectDir = resolve21(projectDirArg);
461709
462230
  const result = await getComposePrompts({
461710
462231
  projectDir,
@@ -461712,18 +462233,21 @@ sceneCommand.command("compose-prompts").description("Emit the per-beat compose p
461712
462233
  });
461713
462234
  if (!result.success) {
461714
462235
  if (isJsonMode()) {
461715
- outputResult({
462236
+ outputSuccess({
461716
462237
  command: "scene compose-prompts",
461717
- ...result
462238
+ startedAt,
462239
+ data: { ...result }
461718
462240
  });
461719
- process.exit(1);
462241
+ process.exitCode = 1;
462242
+ return;
461720
462243
  }
461721
462244
  exitWithError(generalError(result.error ?? "compose-prompts failed"));
461722
462245
  }
461723
462246
  if (isJsonMode()) {
461724
- outputResult({
462247
+ outputSuccess({
461725
462248
  command: "scene compose-prompts",
461726
- ...result
462249
+ startedAt,
462250
+ data: { ...result }
461727
462251
  });
461728
462252
  return;
461729
462253
  }
@@ -461757,20 +462281,23 @@ sceneCommand.command("compose-prompts").description("Emit the per-beat compose p
461757
462281
  console.log(source_default.dim("Re-run with --json to get the full per-beat userPrompt + cues for direct consumption."));
461758
462282
  });
461759
462283
  sceneCommand.command("styles").description("List vendored visual styles (or show one) for DESIGN.md seeding").argument("[name]", "Style name to inspect (omit to list all)").action((name) => {
462284
+ const startedAt = Date.now();
461760
462285
  if (!name) {
461761
462286
  const all = listVisualStyles();
461762
462287
  if (isJsonMode()) {
461763
- outputResult({
461764
- success: true,
462288
+ outputSuccess({
461765
462289
  command: "scene styles",
461766
- count: all.length,
461767
- styles: all.map((s) => ({
461768
- name: s.name,
461769
- slug: s.slug,
461770
- designer: s.designer,
461771
- mood: s.mood,
461772
- bestFor: s.bestFor
461773
- }))
462290
+ startedAt,
462291
+ data: {
462292
+ count: all.length,
462293
+ styles: all.map((s) => ({
462294
+ name: s.name,
462295
+ slug: s.slug,
462296
+ designer: s.designer,
462297
+ mood: s.mood,
462298
+ bestFor: s.bestFor
462299
+ }))
462300
+ }
461774
462301
  });
461775
462302
  return;
461776
462303
  }
@@ -461798,7 +462325,11 @@ sceneCommand.command("styles").description("List vendored visual styles (or show
461798
462325
  return;
461799
462326
  }
461800
462327
  if (isJsonMode()) {
461801
- outputResult({ success: true, command: "scene styles", style });
462328
+ outputSuccess({
462329
+ command: "scene styles",
462330
+ startedAt,
462331
+ data: { style }
462332
+ });
461802
462333
  return;
461803
462334
  }
461804
462335
  console.log();
@@ -461820,6 +462351,7 @@ sceneCommand.command("styles").description("List vendored visual styles (or show
461820
462351
  console.log(source_default.dim("Seed DESIGN.md:"), source_default.cyan(`vibe scene init <dir> --visual-style "${style.name}"`));
461821
462352
  });
461822
462353
  sceneCommand.command("add").description("Add a new scene to a project: AI narration + image + per-scene HTML").argument("<name>", "Scene name (slugified into the composition id)").option("--style <preset>", `Style preset: ${SCENE_PRESETS.join(", ")}`, "simple").option("--narration <text>", "Narration text (or path to a .txt file). Drives TTS + scene duration.").option("--narration-file <path>", "Existing narration audio file (.wav/.mp3). Skips TTS \u2014 useful with hyperframes tts, Mac say, or other external tools.").option("-d, --duration <sec>", "Explicit scene duration in seconds (overrides narration audio)").option("--visuals <prompt>", "Image prompt \u2014 generates assets/scene-<id>.png via the configured image provider").option("--headline <text>", "Visible headline (defaults to the humanised scene name)").option("--kicker <text>", "Small label above the headline (explainer / product-shot)").option("--insert-into <path>", "Root composition file to update", "index.html").option("--project <dir>", "Project directory", ".").option("--image-provider <name>", "Image provider: gemini, openai", "gemini").option("--tts <provider>", "TTS provider: auto, elevenlabs, kokoro (default auto \u2014 picks ElevenLabs when key set, else Kokoro local)", "auto").option("--voice <id>", "Voice id (ElevenLabs name/id, or Kokoro id like af_heart, am_michael)").option("--no-audio", "Skip TTS even when --narration is provided (useful for tests/agent dry runs)").option("--no-image", "Skip image generation even when --visuals is provided").option("--no-transcribe", "Skip Whisper word-level transcribe step (no transcript-<id>.json emitted)").option("--transcribe-language <code>", "BCP-47 language code passed to Whisper (e.g. en, ko)").option("--force", "Overwrite an existing compositions/scene-<id>.html").option("--dry-run", "Preview parameters without writing files or calling APIs").action(async (name, options) => {
462354
+ const startedAt = Date.now();
461823
462355
  if (options.style) options.style = validatePreset(options.style);
461824
462356
  if (options.duration !== void 0) options.duration = validateDuration(options.duration);
461825
462357
  let tts;
@@ -461830,25 +462362,28 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
461830
462362
  }
461831
462363
  if (options.dryRun) {
461832
462364
  const id = slugifySceneName(name);
461833
- outputResult({
461834
- dryRun: true,
462365
+ outputSuccess({
461835
462366
  command: "scene add",
461836
- params: {
461837
- name,
461838
- id,
461839
- preset: options.style,
461840
- narration: !!options.narration,
461841
- visuals: !!options.visuals,
461842
- duration: options.duration,
461843
- headline: options.headline,
461844
- kicker: options.kicker,
461845
- project: options.project,
461846
- insertInto: options.insertInto,
461847
- imageProvider: options.imageProvider,
461848
- tts,
461849
- audio: options.audio,
461850
- // commander sets `audio: false` when --no-audio is passed
461851
- image: options.image
462367
+ startedAt,
462368
+ dryRun: true,
462369
+ data: {
462370
+ params: {
462371
+ name,
462372
+ id,
462373
+ preset: options.style,
462374
+ narration: !!options.narration,
462375
+ visuals: !!options.visuals,
462376
+ duration: options.duration,
462377
+ headline: options.headline,
462378
+ kicker: options.kicker,
462379
+ project: options.project,
462380
+ insertInto: options.insertInto,
462381
+ imageProvider: options.imageProvider,
462382
+ tts,
462383
+ audio: options.audio,
462384
+ // commander sets `audio: false` when --no-audio is passed
462385
+ image: options.image
462386
+ }
461852
462387
  }
461853
462388
  });
461854
462389
  return;
@@ -461883,9 +462418,10 @@ sceneCommand.command("add").description("Add a new scene to a project: AI narrat
461883
462418
  exitWithError(generalError(result.error ?? "Scene add failed"));
461884
462419
  }
461885
462420
  if (isJsonMode()) {
461886
- outputResult({
462421
+ outputSuccess({
461887
462422
  command: "scene add",
461888
- ...result
462423
+ startedAt,
462424
+ data: { ...result }
461889
462425
  });
461890
462426
  return;
461891
462427
  }
@@ -462180,7 +462716,8 @@ async function executeSceneAdd(opts) {
462180
462716
  transcriptWordCount
462181
462717
  };
462182
462718
  }
462183
- sceneCommand.command("lint").description("Validate scene HTML against Hyperframes rules (in-process, no Chrome required)").argument("[root]", "Root composition file relative to --project", "index.html").option("--project <dir>", "Project directory", ".").option("--fix", 'Apply mechanical auto-fixes (currently: missing class="clip")').action(async (root2, options) => {
462719
+ sceneCommand.command("lint").description("Validate scene HTML against composition rules (in-process, no Chrome required)").argument("[root]", "Root composition file relative to --project", "index.html").option("--project <dir>", "Project directory", ".").option("--fix", 'Apply mechanical auto-fixes (currently: missing class="clip")').action(async (root2, options) => {
462720
+ const startedAt = Date.now();
462184
462721
  const projectDir = resolve21(options.project);
462185
462722
  if (!await rootExists(projectDir, root2)) {
462186
462723
  exitWithError(generalError(
@@ -462198,11 +462735,12 @@ sceneCommand.command("lint").description("Validate scene HTML against Hyperframe
462198
462735
  exitWithError(generalError(`Lint failed: ${msg}`));
462199
462736
  }
462200
462737
  if (isJsonMode()) {
462201
- outputResult({
462738
+ outputSuccess({
462202
462739
  command: "scene lint",
462203
- ...result
462740
+ startedAt,
462741
+ data: { ...result }
462204
462742
  });
462205
- if (!result.ok) process.exit(1);
462743
+ if (!result.ok) process.exitCode = 1;
462206
462744
  return;
462207
462745
  }
462208
462746
  if (result.ok && result.warningCount === 0 && result.infoCount === 0) {
@@ -462236,7 +462774,7 @@ sceneCommand.command("lint").description("Validate scene HTML against Hyperframe
462236
462774
  console.log(` ${source_default.green("\u2714")} ${fx.file} ${source_default.dim(fx.codes.join(", "))}`);
462237
462775
  }
462238
462776
  }
462239
- if (!result.ok) process.exit(1);
462777
+ if (!result.ok) process.exitCode = 1;
462240
462778
  });
462241
462779
  function severityTag(severity) {
462242
462780
  if (severity === "error") return source_default.red("\u2718 error ");
@@ -462273,23 +462811,27 @@ function validateWorkers(value) {
462273
462811
  return n;
462274
462812
  }
462275
462813
  sceneCommand.command("render").description("Render a scene project to MP4/WebM/MOV via the Hyperframes producer (requires Chrome)").argument("[root]", "Root composition file relative to --project", "index.html").option("--project <dir>", "Project directory", ".").option("-o, --out <path>", "Output file (default: renders/<name>-<timestamp>.<format>)").option("--fps <n>", `Frames per second: ${VALID_FPS.join("|")}`, "30").option("--quality <q>", `Quality preset: ${VALID_QUALITIES.join("|")}`, "standard").option("--format <f>", `Output container: ${VALID_FORMATS.join("|")}`, "mp4").option("--workers <n>", "Capture workers (1-16, default 1)", "1").option("--dry-run", "Preview parameters without rendering").action(async (root2, options) => {
462814
+ const startedAt = Date.now();
462276
462815
  const fps = validateFps(options.fps);
462277
462816
  const quality = validateQuality(options.quality);
462278
462817
  const format4 = validateFormat(options.format);
462279
462818
  const workers = validateWorkers(options.workers);
462280
462819
  const projectDir = resolve21(options.project);
462281
462820
  if (options.dryRun) {
462282
- outputResult({
462283
- dryRun: true,
462821
+ outputSuccess({
462284
462822
  command: "scene render",
462285
- params: {
462286
- projectDir,
462287
- root: root2,
462288
- output: options.out,
462289
- fps,
462290
- quality,
462291
- format: format4,
462292
- workers
462823
+ startedAt,
462824
+ dryRun: true,
462825
+ data: {
462826
+ params: {
462827
+ projectDir,
462828
+ root: root2,
462829
+ output: options.out,
462830
+ fps,
462831
+ quality,
462832
+ format: format4,
462833
+ workers
462834
+ }
462293
462835
  }
462294
462836
  });
462295
462837
  return;
@@ -462310,13 +462852,22 @@ sceneCommand.command("render").description("Render a scene project to MP4/WebM/M
462310
462852
  if (!result.success) {
462311
462853
  spinner2?.fail("Render failed");
462312
462854
  if (isJsonMode()) {
462313
- outputResult({ command: "scene render", ...result });
462314
- process.exit(1);
462855
+ outputSuccess({
462856
+ command: "scene render",
462857
+ startedAt,
462858
+ data: { ...result }
462859
+ });
462860
+ process.exitCode = 1;
462861
+ return;
462315
462862
  }
462316
462863
  exitWithError(generalError(result.error ?? "Render failed"));
462317
462864
  }
462318
462865
  if (isJsonMode()) {
462319
- outputResult({ command: "scene render", ...result });
462866
+ outputSuccess({
462867
+ command: "scene render",
462868
+ startedAt,
462869
+ data: { ...result }
462870
+ });
462320
462871
  return;
462321
462872
  }
462322
462873
  spinner2?.succeed(source_default.green(`Render complete: ${result.outputPath}`));
@@ -462336,25 +462887,29 @@ sceneCommand.command("render").description("Render a scene project to MP4/WebM/M
462336
462887
  }
462337
462888
  });
462338
462889
  sceneCommand.command("build").description("One-shot: read STORYBOARD.md cues, dispatch TTS + image-gen per beat, compose, render to MP4 (v0.60)").argument("[project-dir]", "Project directory containing STORYBOARD.md", ".").option("--mode <mode>", "Build mode: agent (host-agent authors HTML) | batch (CLI's internal LLM authors HTML) | auto (agent if any host detected) [Plan H \u2014 Phase 3]", "auto").option("--effort <level>", "Compose effort tier (batch mode only): low|medium|high", "medium").option("--composer <provider>", "LLM that composes scene HTML in batch mode: claude|openai|gemini (default: auto-resolve from available API keys, claude > gemini > openai)").option("--skip-narration", "Don't dispatch TTS even when beats declare narration cues").option("--skip-backdrop", "Don't dispatch image-gen even when beats declare backdrop cues").option("--skip-render", "Compose only \u2014 don't render to MP4").option("--tts <provider>", "TTS provider: auto|elevenlabs|kokoro (overrides frontmatter)").option("--voice <id>", "Voice id (provider-specific \u2014 overrides frontmatter)").option("--image-provider <name>", "Image provider: openai (only one supported in v0.60)").option("--quality <q>", "Image quality: standard|hd", "hd").option("--image-size <s>", "Image size: 1024x1024|1536x1024|1024x1536", "1536x1024").option("--force", "Re-dispatch primitives even when assets already exist").option("--dry-run", "Preview parameters without dispatching").action(async (projectDirArg, options) => {
462890
+ const startedAt = Date.now();
462339
462891
  const projectDir = resolve21(projectDirArg);
462340
462892
  if (options.dryRun) {
462341
- outputResult({
462342
- dryRun: true,
462893
+ outputSuccess({
462343
462894
  command: "scene build",
462344
- params: {
462345
- projectDir,
462346
- mode: options.mode,
462347
- effort: options.effort,
462348
- composer: options.composer,
462349
- skipNarration: options.skipNarration ?? false,
462350
- skipBackdrop: options.skipBackdrop ?? false,
462351
- skipRender: options.skipRender ?? false,
462352
- ttsProvider: options.tts,
462353
- voice: options.voice,
462354
- imageProvider: options.imageProvider,
462355
- imageQuality: options.quality,
462356
- imageSize: options.imageSize,
462357
- force: options.force ?? false
462895
+ startedAt,
462896
+ dryRun: true,
462897
+ data: {
462898
+ params: {
462899
+ projectDir,
462900
+ mode: options.mode,
462901
+ effort: options.effort,
462902
+ composer: options.composer,
462903
+ skipNarration: options.skipNarration ?? false,
462904
+ skipBackdrop: options.skipBackdrop ?? false,
462905
+ skipRender: options.skipRender ?? false,
462906
+ ttsProvider: options.tts,
462907
+ voice: options.voice,
462908
+ imageProvider: options.imageProvider,
462909
+ imageQuality: options.quality,
462910
+ imageSize: options.imageSize,
462911
+ force: options.force ?? false
462912
+ }
462358
462913
  }
462359
462914
  });
462360
462915
  return;
@@ -462408,13 +462963,22 @@ sceneCommand.command("build").description("One-shot: read STORYBOARD.md cues, di
462408
462963
  if (!result.success) {
462409
462964
  spinner2?.fail(`Build failed: ${result.error}`);
462410
462965
  if (isJsonMode()) {
462411
- outputResult({ command: "scene build", ...result });
462412
- process.exit(1);
462966
+ outputSuccess({
462967
+ command: "scene build",
462968
+ startedAt,
462969
+ data: { ...result }
462970
+ });
462971
+ process.exitCode = 1;
462972
+ return;
462413
462973
  }
462414
462974
  exitWithError(generalError(result.error ?? "Build failed"));
462415
462975
  }
462416
462976
  if (isJsonMode()) {
462417
- outputResult({ command: "scene build", ...result });
462977
+ outputSuccess({
462978
+ command: "scene build",
462979
+ startedAt,
462980
+ data: { ...result }
462981
+ });
462418
462982
  return;
462419
462983
  }
462420
462984
  if (result.phase === "needs-author") {
@@ -462439,7 +463003,7 @@ sceneCommand.command("build").description("One-shot: read STORYBOARD.md cues, di
462439
463003
  }
462440
463004
  }
462441
463005
  console.log();
462442
- console.log(source_default.dim("Once you've authored each beat's HTML, re-run `vibe scene build` to lint + render."));
463006
+ console.log(source_default.dim("Once you've authored each beat's HTML, re-run `vibe build` to lint + render."));
462443
463007
  console.log(source_default.dim("Or pass `--mode batch` to use the internal LLM compose path instead."));
462444
463008
  return;
462445
463009
  }
@@ -462578,7 +463142,7 @@ var sceneInitTool = defineTool({
462578
463142
  name: "scene_init",
462579
463143
  category: "scene",
462580
463144
  cost: "free",
462581
- description: "Scaffold a new bilingual VibeFrame + Hyperframes scene project. Creates index.html, hyperframes.json, vibe.project.yaml, compositions/, assets/, .gitignore, and a project-local CLAUDE.md. Idempotent: re-running on an existing Hyperframes project merges hyperframes.json instead of overwriting. No API keys required.",
463145
+ description: "Scaffold a new VibeFrame video scene project. Supports minimal, agent, and full profiles; full includes the current HTML render backend metadata. Idempotent: re-running keeps user-authored files and merges backend config instead of overwriting. No API keys required.",
462582
463146
  schema: sceneInitSchema,
462583
463147
  async execute(args, ctx) {
462584
463148
  const dir = resolve23(ctx.workingDirectory, args.dir);
@@ -464736,38 +465300,44 @@ function findUnresolvedRefs(params, availableStepIds) {
464736
465300
 
464737
465301
  // ../cli/src/pipeline/executor.ts
464738
465302
  init_output();
464739
- var ACTION_TO_COMMAND = {
464740
- "generate-image": "generate image",
464741
- "generate-video": "generate video",
464742
- "generate-tts": "generate speech",
464743
- "generate-sfx": "generate sound-effect",
464744
- "generate-music": "generate music",
464745
- "generate-storyboard": "generate storyboard",
464746
- "generate-motion": "generate motion",
464747
- "edit-silence-cut": "edit silence-cut",
464748
- "edit-jump-cut": "edit jump-cut",
464749
- "edit-caption": "edit caption",
464750
- "edit-noise-reduce": "edit noise-reduce",
464751
- "edit-fade": "edit fade",
464752
- "edit-translate-srt": "edit translate-srt",
464753
- "edit-text-overlay": "edit text-overlay",
464754
- "edit-grade": "edit grade",
464755
- "edit-speed-ramp": "edit speed-ramp",
464756
- "edit-reframe": "edit reframe",
464757
- "edit-interpolate": "edit interpolate",
464758
- "edit-upscale": "edit upscale-video",
464759
- "edit-image": "edit image",
464760
- "audio-transcribe": "audio transcribe",
464761
- "detect-scenes": "detect scenes",
464762
- "detect-silence": "detect silence",
464763
- "detect-beats": "detect beats",
464764
- "analyze-media": "analyze media",
464765
- "analyze-video": "analyze video",
464766
- "review-video": "analyze review",
464767
- "compose-scenes-with-skills": "compose scenes with skills"
465303
+ var ACTION_METADATA = {
465304
+ "generate-image": { id: "generate-image", title: "Generate image", category: "generate", command: "generate image", outputs: ["image"] },
465305
+ "generate-video": { id: "generate-video", title: "Generate video", category: "generate", command: "generate video", outputs: ["video"], requiredKeys: ["provider-dependent"] },
465306
+ "generate-tts": { id: "generate-tts", title: "Generate speech", category: "generate", command: "generate speech", outputs: ["audio"] },
465307
+ "generate-sfx": { id: "generate-sfx", title: "Generate sound effect", category: "generate", command: "generate sound-effect", outputs: ["audio"] },
465308
+ "generate-music": { id: "generate-music", title: "Generate music", category: "generate", command: "generate music", outputs: ["audio"] },
465309
+ "generate-storyboard": { id: "generate-storyboard", title: "Generate storyboard", category: "generate", command: "generate storyboard", outputs: ["storyboard"] },
465310
+ "generate-motion": { id: "generate-motion", title: "Generate motion", category: "generate", command: "generate motion", outputs: ["code", "video"] },
465311
+ "edit-silence-cut": { id: "edit-silence-cut", title: "Cut silence", category: "edit", command: "edit silence-cut", outputs: ["video"] },
465312
+ "edit-jump-cut": { id: "edit-jump-cut", title: "Jump cut", category: "edit", command: "edit jump-cut", outputs: ["video"] },
465313
+ "edit-caption": { id: "edit-caption", title: "Caption video", category: "edit", command: "edit caption", outputs: ["video", "srt"] },
465314
+ "edit-noise-reduce": { id: "edit-noise-reduce", title: "Reduce noise", category: "edit", command: "edit noise-reduce", outputs: ["video"] },
465315
+ "edit-fade": { id: "edit-fade", title: "Add fade", category: "edit", command: "edit fade", outputs: ["video"] },
465316
+ "edit-translate-srt": { id: "edit-translate-srt", title: "Translate subtitles", category: "edit", command: "edit translate-srt", outputs: ["srt"] },
465317
+ "edit-text-overlay": { id: "edit-text-overlay", title: "Add text overlay", category: "edit", command: "edit text-overlay", outputs: ["video"] },
465318
+ "edit-grade": { id: "edit-grade", title: "Color grade", category: "edit", command: "edit grade", outputs: ["video"] },
465319
+ "edit-speed-ramp": { id: "edit-speed-ramp", title: "Speed ramp", category: "edit", command: "edit speed-ramp", outputs: ["video"] },
465320
+ "edit-reframe": { id: "edit-reframe", title: "Reframe video", category: "edit", command: "edit reframe", outputs: ["video"] },
465321
+ "edit-interpolate": { id: "edit-interpolate", title: "Interpolate frames", category: "edit", command: "edit interpolate", outputs: ["video"] },
465322
+ "edit-upscale": { id: "edit-upscale", title: "Upscale video", category: "edit", command: "edit upscale-video", outputs: ["video"] },
465323
+ "edit-image": { id: "edit-image", title: "Edit image", category: "edit", command: "edit image", outputs: ["image"] },
465324
+ "audio-transcribe": { id: "audio-transcribe", title: "Transcribe audio", category: "audio", command: "audio transcribe", outputs: ["transcript", "srt"] },
465325
+ "audio-isolate": { id: "audio-isolate", title: "Isolate audio", category: "audio", outputs: ["audio"] },
465326
+ "audio-dub": { id: "audio-dub", title: "Dub audio", category: "audio", outputs: ["audio", "video"] },
465327
+ "audio-duck": { id: "audio-duck", title: "Duck audio", category: "audio", outputs: ["video"] },
465328
+ "detect-scenes": { id: "detect-scenes", title: "Detect scenes", category: "detect", command: "detect scenes", outputs: ["json"] },
465329
+ "detect-silence": { id: "detect-silence", title: "Detect silence", category: "detect", command: "detect silence", outputs: ["json"] },
465330
+ "detect-beats": { id: "detect-beats", title: "Detect beats", category: "detect", command: "detect beats", outputs: ["json"] },
465331
+ "analyze-media": { id: "analyze-media", title: "Analyze media", category: "analyze", command: "analyze media", outputs: ["json"] },
465332
+ "analyze-video": { id: "analyze-video", title: "Analyze video", category: "analyze", command: "analyze video", outputs: ["json"] },
465333
+ "review-video": { id: "review-video", title: "Review video", category: "analyze", command: "analyze review", outputs: ["json"] },
465334
+ "compose-scenes-with-skills": { id: "compose-scenes-with-skills", title: "Compose scenes with skills", category: "scene", command: "compose scenes with skills", outputs: ["html"] },
465335
+ "scene-build": { id: "scene-build", title: "Build scene project", category: "scene", outputs: ["video", "html", "assets"] },
465336
+ "scene-render": { id: "scene-render", title: "Render scene project", category: "scene", outputs: ["video"] },
465337
+ export: { id: "export", title: "Export project", category: "export", outputs: ["video"] }
464768
465338
  };
464769
465339
  function maxCostFor(action) {
464770
- const cmd = ACTION_TO_COMMAND[action];
465340
+ const cmd = ACTION_METADATA[action]?.command;
464771
465341
  if (!cmd) return 0;
464772
465342
  return COST_ESTIMATES[cmd]?.max ?? 0;
464773
465343
  }
@@ -464792,7 +465362,15 @@ async function ensureActionsRegistered() {
464792
465362
  registerAction("generate-video", async (params, outputDir) => {
464793
465363
  const { executeVideoGenerate: executeVideoGenerate2 } = await Promise.resolve().then(() => (init_ai_video(), ai_video_exports));
464794
465364
  const output3 = getOutput(params, outputDir, "video.mp4");
464795
- const r = await executeVideoGenerate2({ prompt: params.prompt, provider: params.provider, image: params.image, duration: params.duration, ratio: params.ratio, output: output3, wait: true });
465365
+ const r = await executeVideoGenerate2({
465366
+ prompt: params.prompt,
465367
+ provider: params.provider,
465368
+ image: params.image,
465369
+ duration: params.duration,
465370
+ ratio: params.ratio,
465371
+ output: output3,
465372
+ wait: true
465373
+ });
464796
465374
  return { id: "", action: "generate-video", success: r.success, output: r.outputPath || r.videoUrl, data: { taskId: r.taskId, provider: r.provider, videoUrl: r.videoUrl }, error: r.error };
464797
465375
  });
464798
465376
  registerAction("generate-tts", async (params, outputDir) => {
@@ -465924,6 +466502,7 @@ Cost: Free (no API keys needed). Requires FFmpeg.
465924
466502
  GIF format: 15fps, no audio, looping. Good for previews and sharing.
465925
466503
  Custom flags (--bitrate, --fps, --resolution, --codec) override preset values.
465926
466504
  Run 'vibe schema export' for structured parameter info.`).action(async (projectPath, options) => {
466505
+ const startedAt = Date.now();
465927
466506
  const spinner2 = ora("Checking FFmpeg...").start();
465928
466507
  try {
465929
466508
  if (options.output) {
@@ -465941,27 +466520,30 @@ Run 'vibe schema export' for structured parameter info.`).action(async (projectP
465941
466520
  exitWithError(usageError(overrideError));
465942
466521
  }
465943
466522
  if (options.dryRun) {
465944
- outputResult({
465945
- dryRun: true,
466523
+ outputSuccess({
465946
466524
  command: "export",
465947
- params: {
465948
- project: projectPath,
465949
- output: options.output || null,
465950
- format: options.format,
465951
- preset: options.preset,
465952
- overwrite: options.overwrite,
465953
- gapFill: options.gapFill,
465954
- backend: options.backend,
465955
- bitrate: options.bitrate ?? null,
465956
- fps: customOverrides.fps ?? null,
465957
- resolution: options.resolution ?? null,
465958
- codec: options.codec ?? null
466525
+ startedAt,
466526
+ dryRun: true,
466527
+ data: {
466528
+ params: {
466529
+ project: projectPath,
466530
+ output: options.output || null,
466531
+ format: options.format,
466532
+ preset: options.preset,
466533
+ overwrite: options.overwrite,
466534
+ gapFill: options.gapFill,
466535
+ backend: options.backend,
466536
+ bitrate: options.bitrate ?? null,
466537
+ fps: customOverrides.fps ?? null,
466538
+ resolution: options.resolution ?? null,
466539
+ codec: options.codec ?? null
466540
+ }
465959
466541
  }
465960
466542
  });
465961
466543
  return;
465962
466544
  }
465963
466545
  if (options.backend === "hyperframes") {
465964
- await runHyperframesExport(projectPath, options, spinner2);
466546
+ await runHyperframesExport(projectPath, options, spinner2, startedAt);
465965
466547
  return;
465966
466548
  }
465967
466549
  const ffmpegPath = await findFFmpeg();
@@ -466024,6 +466606,25 @@ Run 'vibe schema export' for structured parameter info.`).action(async (projectP
466024
466606
  spinner2.text = `Encoding... ${progress}%`;
466025
466607
  });
466026
466608
  spinner2.succeed(source_default.green(`Exported: ${outputPath}`));
466609
+ if (isJsonMode()) {
466610
+ outputSuccess({
466611
+ command: "export",
466612
+ startedAt,
466613
+ data: {
466614
+ outputPath,
466615
+ backend: "ffmpeg",
466616
+ format: options.format,
466617
+ preset: options.preset,
466618
+ resolution: presetSettings.resolution,
466619
+ duration: summary.duration,
466620
+ clipCount: summary.clipCount,
466621
+ bitrate: options.bitrate ?? null,
466622
+ fps: options.fps ?? null,
466623
+ codec: options.codec ?? null
466624
+ }
466625
+ });
466626
+ return;
466627
+ }
466027
466628
  console.log();
466028
466629
  console.log(source_default.dim(" Duration:"), `${summary.duration.toFixed(1)}s`);
466029
466630
  console.log(source_default.dim(" Clips:"), summary.clipCount);
@@ -466548,13 +467149,13 @@ function getPresetSettings(preset, aspectRatio) {
466548
467149
  }
466549
467150
  return settings;
466550
467151
  }
466551
- async function runHyperframesExport(projectPath, options, spinner2) {
467152
+ async function runHyperframesExport(projectPath, options, spinner2, startedAt) {
466552
467153
  spinner2.text = "Loading project...";
466553
467154
  const { readFile: readFile34 } = await import("node:fs/promises");
466554
467155
  const { resolve: resolve64, basename: basename18 } = await import("node:path");
466555
467156
  const { Project: Project2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
466556
467157
  const { createHyperframesBackend: createHyperframesBackend2 } = await Promise.resolve().then(() => (init_hyperframes(), hyperframes_exports));
466557
- const { exitWithError: exitWithError2, generalError: generalError2, outputResult: outputResult2 } = await Promise.resolve().then(() => (init_output(), output_exports));
467158
+ const { exitWithError: exitWithError2, generalError: generalError2, outputSuccess: outputSuccess2 } = await Promise.resolve().then(() => (init_output(), output_exports));
466558
467159
  const chalk2 = (await Promise.resolve().then(() => (init_source(), source_exports))).default;
466559
467160
  const filePath = resolve64(process.cwd(), projectPath);
466560
467161
  const content = await readFile34(filePath, "utf-8");
@@ -466580,10 +467181,10 @@ async function runHyperframesExport(projectPath, options, spinner2) {
466580
467181
  return;
466581
467182
  }
466582
467183
  spinner2.succeed(chalk2.green(`Exported: ${result.outputPath}`));
466583
- outputResult2({
466584
- success: true,
467184
+ outputSuccess2({
466585
467185
  command: "export",
466586
- result: {
467186
+ startedAt,
467187
+ data: {
466587
467188
  outputPath: result.outputPath,
466588
467189
  backend: "hyperframes",
466589
467190
  durationMs: result.durationMs,
@@ -467499,7 +468100,7 @@ A scene project is a directory that is **bilingual**: it works with both
467499
468100
  and a paused GSAP timeline. Cheap to edit, cheap to lint, expensive only
467500
468101
  at render.
467501
468102
 
467502
- \`vibe scene build\` (v0.60+) is the supported one-shot driver from a
468103
+ \`vibe build\` (v0.60+) is the supported one-shot driver from a
467503
468104
  written storyboard to an MP4. Plan H (v0.70) added \`--mode agent\` so the
467504
468105
  host agent itself authors the per-beat HTML \u2014 no internal LLM call.
467505
468106
 
@@ -467507,19 +468108,19 @@ host agent itself authors the per-beat HTML \u2014 no internal LLM call.
467507
468108
 
467508
468109
  | Path | Command | When to use |
467509
468110
  |---|---|---|
467510
- | **One-shot (default, v0.60+)** | \`vibe scene build [project-dir]\` | STORYBOARD.md has YAML frontmatter + per-beat cues |
467511
- | **High-craft (manual)** | \`DESIGN.md\` + Hyperframes skill in your agent | Maximum control: hand-author each scene |
468111
+ | **One-shot (default, v0.60+)** | \`vibe build [project-dir]\` | STORYBOARD.md has YAML frontmatter + per-beat cues |
468112
+ | **High-craft (manual)** | \`DESIGN.md\` + local composition rules in your agent | Maximum control: hand-author each scene |
467512
468113
  | **Quick draft** | \`vibe scene add --style <preset>\` | No agent or no API keys; fast iteration |
467513
468114
 
467514
- Recommend \`vibe scene build\` whenever the user has a STORYBOARD with
468115
+ Recommend \`vibe build\` whenever the user has a STORYBOARD with
467515
468116
  narration / backdrop intent.
467516
468117
 
467517
468118
  ## High-craft path
467518
468119
 
467519
- 1. \`vibe scene init my-promo --visual-style "Swiss Pulse"\` \u2014 seeds
468120
+ 1. \`vibe init my-promo --visual-style "Swiss Pulse"\` \u2014 seeds
467520
468121
  \`DESIGN.md\` (palette, typography, motion, transitions) plus the
467521
468122
  \`vibe.project.yaml\` / \`hyperframes.json\` / \`index.html\` scaffold.
467522
- In Plan H this **also installs the Hyperframes skill** at the
468123
+ In Plan H this **also installs local composition rules** at the
467523
468124
  right place for your host (\`.claude/skills/hyperframes/\` for Claude
467524
468125
  Code, \`.cursor/rules/hyperframes.mdc\` for Cursor, universal
467525
468126
  \`SKILL.md\` for everyone else).
@@ -467530,7 +468131,7 @@ narration / backdrop intent.
467530
468131
  4. Author each scene HTML directly under \`compositions/scene-<id>.html\`
467531
468132
  using the rules from steps 2 and 3. The skill enforces the visual
467532
468133
  identity contract \u2014 scenes that contradict DESIGN.md fail lint.
467533
- 5. \`vibe scene lint --fix\` for mechanical issues, \`vibe scene render\`
468134
+ 5. \`vibe scene lint --fix\` for mechanical issues, \`vibe render my-promo\`
467534
468135
  to MP4.
467535
468136
 
467536
468137
  ## Quick-draft path
@@ -467540,7 +468141,7 @@ vibe scene init my-promo -r 16:9 -d 30
467540
468141
  vibe scene add intro --style announcement \\
467541
468142
  --headline "Ship videos, not clicks"
467542
468143
  vibe scene lint
467543
- vibe scene render
468144
+ vibe render my-promo
467544
468145
  \`\`\`
467545
468146
 
467546
468147
  \`vibe scene init\` is **idempotent** \u2014 running it on an existing
@@ -467552,12 +468153,12 @@ Safe to invoke on user-provided projects.
467552
468153
  \`\`\`bash
467553
468154
  vibe scene init <dir> [-r 16:9|9:16|1:1|4:5] [-d <sec>] [--visual-style "<name>"]
467554
468155
  vibe scene styles [<name>] # list / show vendored visual identities
467555
- vibe scene install-skill [<dir>] [--host all] # retroactive Hyperframes-skill install
468156
+ vibe scene install-skill [<dir>] [--host all] # retroactive composition-rules install
467556
468157
  vibe scene add <name> --style <preset> [...]
467557
468158
  vibe scene compose-prompts [<dir>] [--beat <id>] # H2: emit plan, no LLM call
467558
468159
  vibe scene lint [<root>] [--json] [--fix]
467559
468160
  vibe scene render [<root>] [--fps 30] [--quality standard] [--format mp4]
467560
- vibe scene build [<dir>] [--mode agent|batch|auto] # H3 dispatch
468161
+ vibe build [<dir>] [--mode agent|batch|auto] # H3 dispatch
467561
468162
  \`\`\`
467562
468163
 
467563
468164
  ## Style presets (for \`vibe scene add --style\`)
@@ -467575,18 +468176,18 @@ from the generated TTS audio.
467575
468176
  ## STORYBOARD-to-MP4 (one command, v0.60+)
467576
468177
 
467577
468178
  \`\`\`bash
467578
- vibe scene init my-promo --visual-style "Swiss Pulse" -d 12
468179
+ vibe init my-promo --visual-style "Swiss Pulse" -d 12
467579
468180
  # (edit STORYBOARD.md with per-beat YAML cues \u2014 narration, backdrop, duration)
467580
- vibe scene build my-promo
468181
+ vibe build my-promo
467581
468182
  \`\`\`
467582
468183
 
467583
- \`vibe scene build\` reads the STORYBOARD frontmatter + per-beat cues,
468184
+ \`vibe build\` reads the STORYBOARD frontmatter + per-beat cues,
467584
468185
  dispatches TTS + image-gen per beat, then either:
467585
468186
 
467586
468187
  - **\`--mode agent\`** (default when an agent host is detected) \u2014 emits a
467587
468188
  \`needs-author\` plan via \`vibe scene compose-prompts\`. The host agent
467588
468189
  authors each \`compositions/scene-<id>.html\` itself, then re-invoking
467589
- \`vibe scene build\` proceeds to lint + render.
468190
+ \`vibe build\` proceeds to lint + render.
467590
468191
  - **\`--mode batch\`** \u2014 VibeFrame runs an internal LLM (Claude / OpenAI /
467591
468192
  Gemini) to compose the HTML, then renders.
467592
468193
 
@@ -467609,9 +468210,9 @@ and surface the error to the user.
467609
468210
  | Task | Tool |
467610
468211
  |------|------|
467611
468212
  | Generate narration + image, then author scene | \`vibe scene add\` |
467612
- | Generate a full scenes project from a STORYBOARD | \`vibe scene build\` |
468213
+ | Generate a full scenes project from a STORYBOARD | \`vibe build\` |
467613
468214
  | Hand-tweak a single scene's animation | edit \`compositions/<file>.html\` directly |
467614
- | Render the project | \`vibe scene render\` *or* \`npx hyperframes render\` (equivalent) |
468215
+ | Render the project | \`vibe render\` *or* \`vibe scene render\` for lower-level control |
467615
468216
  | Lint | \`vibe scene lint\` *or* \`npx hyperframes lint\` (equivalent) |
467616
468217
 
467617
468218
  The \`vibe\` CLI adds asset generation, AI orchestration, and pipeline
@@ -467695,8 +468296,8 @@ Checkpoints land next to the YAML: \`pipeline.yaml.checkpoint.json\`.
467695
468296
 
467696
468297
  ## Authoring tips
467697
468298
 
467698
- 1. **Start from examples** \u2014 \`examples/demo-pipeline.yaml\` (FFmpeg-only,
467699
- no keys), \`examples/promo-video.yaml\` (AI providers).
468299
+ 1. **Start from a tiny YAML** \u2014 keep it in your project directory and run
468300
+ \`vibe run pipeline.yaml --dry-run\` before spending provider budget.
467700
468301
  2. **Dry-run first** \u2014 you see estimated cost and resolved variable
467701
468302
  graph before spending API credits.
467702
468303
  3. **Keep step ids short and descriptive** (\`intro\`, \`scene1\`, \`voice\`,
@@ -467731,18 +468332,18 @@ var META = {
467731
468332
  title: "Scene authoring with vibe",
467732
468333
  summary: "Author per-scene HTML compositions and render to MP4 (BUILD flow)",
467733
468334
  steps: [
467734
- 'Run `vibe scene init <dir> --visual-style "<style name>"` to scaffold the project + install the Hyperframes skill (Plan H).',
468335
+ 'Run `vibe init <dir> --visual-style "<style name>"` to scaffold the project + install local composition rules.',
467735
468336
  "Edit `STORYBOARD.md` with per-beat YAML cues (narration / backdrop / duration).",
467736
468337
  "Read `SKILL.md` for the framework rules and `DESIGN.md` for the visual-identity hard-gate.",
467737
- "Run `vibe scene build <dir>`. With an agent host detected, the CLI emits a `needs-author` plan; the host agent authors each `compositions/scene-<id>.html` and re-invokes to render.",
467738
- "Run `vibe scene lint --fix` to validate, then `vibe scene render` to produce the MP4."
468338
+ "Run `vibe build <dir>`. With an agent host detected, the CLI emits a `needs-author` plan; the host agent authors each `compositions/scene-<id>.html` and re-invokes to render.",
468339
+ "Run `vibe scene lint --fix` to validate, then `vibe render <dir>` to produce the MP4."
467739
468340
  ],
467740
468341
  relatedCommands: [
467741
- "vibe scene init",
468342
+ "vibe init",
467742
468343
  "vibe scene styles",
467743
468344
  "vibe scene install-skill",
467744
468345
  "vibe scene compose-prompts",
467745
- "vibe scene build",
468346
+ "vibe build",
467746
468347
  "vibe scene lint",
467747
468348
  "vibe scene render",
467748
468349
  "vibe scene add"