@vibeframe/cli 0.111.0 → 0.111.3

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.
@@ -0,0 +1,3 @@
1
+ /** Test-only hook — CI runners have no ffmpeg, production code never calls this. */
2
+ export declare function __setFfmpegToolsForTests(value: boolean | null): void;
3
+ export declare function ffmpegToolsAvailable(): boolean;
@@ -49,6 +49,8 @@ export declare function resolveSyncedBeatDuration(opts: {
49
49
  beatDuration?: number;
50
50
  narrationPath?: string;
51
51
  sceneDurationSec?: number;
52
+ /** Injectable for tests — defaults to the ffprobe-backed audio probe. */
53
+ probeAudioDuration?: (absPath: string) => Promise<number>;
52
54
  }): Promise<number>;
53
55
  /**
54
56
  * Final narration-synced duration per beat for a project on disk. Reads
@@ -0,0 +1 @@
1
+ export {};
@@ -67,6 +67,13 @@ export interface SceneRenderResult {
67
67
  /** Non-fatal warning from the audio mux pass — caller may surface to the user. */
68
68
  audioMuxWarning?: string;
69
69
  reportPath?: string;
70
+ /**
71
+ * True when the pre-render drift guard found narration-synced durations
72
+ * out of step with the composed timeline (e.g. sync stage skipped or run
73
+ * with stale durations) and re-synced root + scene durations first.
74
+ */
75
+ autoSyncApplied?: boolean;
76
+ autoSyncWarning?: string;
70
77
  code?: string;
71
78
  error?: string;
72
79
  retryWith?: string[];
package/dist/index.js CHANGED
@@ -29414,14 +29414,15 @@ async function expectedRootSync(opts) {
29414
29414
  return { block, totalDurationSec: Number(cursor.toFixed(2)), audioRefs };
29415
29415
  }
29416
29416
  async function resolveSyncedBeatDuration(opts) {
29417
- if (opts.sceneDurationSec !== void 0) return Number(opts.sceneDurationSec.toFixed(2));
29418
29417
  const storyboardMin = opts.beatDuration ?? 3;
29419
- if (!opts.narrationPath) return Number(storyboardMin.toFixed(2));
29418
+ const reported = opts.sceneDurationSec;
29419
+ if (!opts.narrationPath) return Number((reported ?? storyboardMin).toFixed(2));
29420
+ const probe = opts.probeAudioDuration ?? getAudioDuration;
29420
29421
  try {
29421
- const audioDuration = await getAudioDuration(join5(opts.projectDir, opts.narrationPath));
29422
- return Number(Math.max(storyboardMin, audioDuration + 0.5).toFixed(2));
29422
+ const audioDuration = await probe(join5(opts.projectDir, opts.narrationPath));
29423
+ return Number(Math.max(storyboardMin, reported ?? 0, audioDuration + 0.5).toFixed(2));
29423
29424
  } catch {
29424
- return Number(storyboardMin.toFixed(2));
29425
+ return Number(Math.max(storyboardMin, reported ?? 0).toFixed(2));
29425
29426
  }
29426
29427
  }
29427
29428
  async function resolveProjectBeatDurations(projectDir) {
@@ -60169,6 +60170,22 @@ var init_build_plan = __esm({
60169
60170
  }
60170
60171
  });
60171
60172
 
60173
+ // src/commands/_shared/ffmpeg-gate.ts
60174
+ function ffmpegToolsAvailable() {
60175
+ if (testOverride !== null) return testOverride;
60176
+ if (cached === null) cached = commandExists("ffmpeg") && commandExists("ffprobe");
60177
+ return cached;
60178
+ }
60179
+ var testOverride, cached;
60180
+ var init_ffmpeg_gate = __esm({
60181
+ "src/commands/_shared/ffmpeg-gate.ts"() {
60182
+ "use strict";
60183
+ init_exec_safe();
60184
+ testOverride = null;
60185
+ cached = null;
60186
+ }
60187
+ });
60188
+
60172
60189
  // src/utils/concurrency.ts
60173
60190
  async function mapWithConcurrency(items, limit, fn) {
60174
60191
  if (!Number.isInteger(limit) || limit <= 0) {
@@ -60527,6 +60544,33 @@ async function executeSceneRender(opts = {}) {
60527
60544
  error: `Project directory not found: ${projectDir}`
60528
60545
  };
60529
60546
  }
60547
+ if (!ffmpegToolsAvailable()) {
60548
+ return {
60549
+ success: false,
60550
+ kind: "render",
60551
+ beat: opts.beatId ?? null,
60552
+ code: "FFMPEG_MISSING",
60553
+ error: "FFmpeg (ffmpeg/ffprobe) was not found on PATH; rendering needs it for audio mux and duration checks. Install it (macOS: `brew install ffmpeg`) and retry."
60554
+ };
60555
+ }
60556
+ let autoSyncApplied = false;
60557
+ let autoSyncWarning;
60558
+ if (!opts.beatId && existsSync48(join24(projectDir, "STORYBOARD.md"))) {
60559
+ try {
60560
+ const rootPlan = await createProjectRootSyncPlan({ projectDir });
60561
+ const subPlans = await createSubCompDurationSyncPlans({
60562
+ projectDir,
60563
+ beats: await loadProjectRootSyncBeats(projectDir)
60564
+ });
60565
+ if (rootPlan.changed || subPlans.some((plan) => plan.changed)) {
60566
+ await executeSceneRepair({ projectDir });
60567
+ autoSyncApplied = true;
60568
+ autoSyncWarning = "Narration-synced durations drifted from the composed timeline; root and scene durations were re-synced before rendering (same pass as `vibe build --stage sync`).";
60569
+ }
60570
+ } catch (error) {
60571
+ autoSyncWarning = `Narration drift check skipped: ${error instanceof Error ? error.message : String(error)}`;
60572
+ }
60573
+ }
60530
60574
  let root = opts.root ?? projectConfig.config.composition.entry;
60531
60575
  if (opts.beatId) {
60532
60576
  const prepared = await prepareBeatRenderRoot({
@@ -60643,7 +60687,9 @@ async function executeSceneRender(opts = {}) {
60643
60687
  format: config2.format,
60644
60688
  audioCount,
60645
60689
  audioMuxApplied,
60646
- audioMuxWarning
60690
+ audioMuxWarning,
60691
+ ...autoSyncApplied ? { autoSyncApplied } : {},
60692
+ ...autoSyncWarning ? { autoSyncWarning } : {}
60647
60693
  };
60648
60694
  if (opts.revealInFinder) {
60649
60695
  try {
@@ -60903,6 +60949,10 @@ var init_scene_render = __esm({
60903
60949
  "src/commands/_shared/scene-render.ts"() {
60904
60950
  "use strict";
60905
60951
  init_chrome();
60952
+ init_ffmpeg_gate();
60953
+ init_root_sync();
60954
+ init_sub_comp_duration_sync();
60955
+ init_scene_repair();
60906
60956
  init_scene_lint();
60907
60957
  init_scene_audio_scan();
60908
60958
  init_scene_audio_mux();
@@ -60972,6 +61022,28 @@ async function executeSceneBuild(opts) {
60972
61022
  providerResolution: buildPlan.providerResolution,
60973
61023
  ...result
60974
61024
  });
61025
+ if (shouldRunStage(selectedStage, "assets") || shouldRunStage(selectedStage, "sync")) {
61026
+ if (!ffmpegToolsAvailable()) {
61027
+ return finishBuildResult({
61028
+ success: false,
61029
+ phase: "failed",
61030
+ mode,
61031
+ selectedStage,
61032
+ code: "FFMPEG_MISSING",
61033
+ error: "ffprobe (part of FFmpeg) was not found on PATH. Narration-synced durations cannot be computed without it.",
61034
+ message: "ffprobe not found on PATH.",
61035
+ suggestion: "Install FFmpeg (macOS: `brew install ffmpeg`), then re-run the build.",
61036
+ recoverable: true,
61037
+ beats: [],
61038
+ stageReports,
61039
+ warnings,
61040
+ retryWith,
61041
+ status: "failed",
61042
+ currentStage: "assets",
61043
+ totalLatencyMs: Date.now() - startedAt
61044
+ });
61045
+ }
61046
+ }
60975
61047
  if (!buildPlan.validation.ok) {
60976
61048
  let invalidBeats = [];
60977
61049
  if (existsSync49(storyboardPath2)) {
@@ -62643,6 +62715,7 @@ var init_scene_build = __esm({
62643
62715
  import_dotenv4 = __toESM(require_main(), 1);
62644
62716
  init_dist2();
62645
62717
  init_audio();
62718
+ init_ffmpeg_gate();
62646
62719
  init_concurrency();
62647
62720
  init_compose_scenes_skills();
62648
62721
  init_compose_prompts();
@@ -67963,10 +68036,13 @@ function mapRenderResultToToolResult(result, projectDir) {
67963
68036
  format: result.format,
67964
68037
  audioCount: result.audioCount,
67965
68038
  audioMuxApplied: result.audioMuxApplied,
67966
- audioMuxWarning: result.audioMuxWarning
68039
+ audioMuxWarning: result.audioMuxWarning,
68040
+ ...result.autoSyncApplied ? { autoSyncApplied: true } : {},
68041
+ ...result.autoSyncWarning ? { autoSyncWarning: result.autoSyncWarning } : {}
67967
68042
  },
67968
68043
  humanLines: [
67969
68044
  `\u2705 Render complete: ${result.absoluteOutputPath ?? result.outputPath}`,
68045
+ ...result.autoSyncApplied ? [` auto-sync: narration-synced durations were re-applied before render`] : [],
67970
68046
  ` duration: ${((result.durationMs ?? 0) / 1e3).toFixed(1)}s`,
67971
68047
  ` frames: ${result.framesRendered ?? "?"}${result.totalFrames ? ` / ${result.totalFrames}` : ""}`,
67972
68048
  ` config: ${result.fps}fps \xB7 ${result.quality} \xB7 ${result.format}`,
@@ -73213,7 +73289,7 @@ init_source();
73213
73289
  // package.json
73214
73290
  var package_default = {
73215
73291
  name: "@vibeframe/cli",
73216
- version: "0.111.0",
73292
+ version: "0.111.3",
73217
73293
  description: "VibeFrame CLI - agentic video workflow runtime",
73218
73294
  private: false,
73219
73295
  license: "MIT",
@@ -30123,14 +30123,15 @@ async function expectedRootSync(opts) {
30123
30123
  return { block, totalDurationSec: Number(cursor.toFixed(2)), audioRefs };
30124
30124
  }
30125
30125
  async function resolveSyncedBeatDuration(opts) {
30126
- if (opts.sceneDurationSec !== void 0) return Number(opts.sceneDurationSec.toFixed(2));
30127
30126
  const storyboardMin = opts.beatDuration ?? 3;
30128
- if (!opts.narrationPath) return Number(storyboardMin.toFixed(2));
30127
+ const reported = opts.sceneDurationSec;
30128
+ if (!opts.narrationPath) return Number((reported ?? storyboardMin).toFixed(2));
30129
+ const probe = opts.probeAudioDuration ?? getAudioDuration;
30129
30130
  try {
30130
- const audioDuration = await getAudioDuration(join7(opts.projectDir, opts.narrationPath));
30131
- return Number(Math.max(storyboardMin, audioDuration + 0.5).toFixed(2));
30131
+ const audioDuration = await probe(join7(opts.projectDir, opts.narrationPath));
30132
+ return Number(Math.max(storyboardMin, reported ?? 0, audioDuration + 0.5).toFixed(2));
30132
30133
  } catch {
30133
- return Number(storyboardMin.toFixed(2));
30134
+ return Number(Math.max(storyboardMin, reported ?? 0).toFixed(2));
30134
30135
  }
30135
30136
  }
30136
30137
  async function resolveProjectBeatDurations(projectDir) {
@@ -31508,6 +31509,22 @@ var init_chrome = __esm({
31508
31509
  }
31509
31510
  });
31510
31511
 
31512
+ // src/commands/_shared/ffmpeg-gate.ts
31513
+ function ffmpegToolsAvailable() {
31514
+ if (testOverride !== null) return testOverride;
31515
+ if (cached === null) cached = commandExists("ffmpeg") && commandExists("ffprobe");
31516
+ return cached;
31517
+ }
31518
+ var testOverride, cached;
31519
+ var init_ffmpeg_gate = __esm({
31520
+ "src/commands/_shared/ffmpeg-gate.ts"() {
31521
+ "use strict";
31522
+ init_exec_safe();
31523
+ testOverride = null;
31524
+ cached = null;
31525
+ }
31526
+ });
31527
+
31511
31528
  // src/commands/_shared/scene-audio-scan.ts
31512
31529
  import { readFile as readFile15 } from "node:fs/promises";
31513
31530
  import { resolve as resolve18 } from "node:path";
@@ -31833,6 +31850,33 @@ async function executeSceneRender(opts = {}) {
31833
31850
  error: `Project directory not found: ${projectDir}`
31834
31851
  };
31835
31852
  }
31853
+ if (!ffmpegToolsAvailable()) {
31854
+ return {
31855
+ success: false,
31856
+ kind: "render",
31857
+ beat: opts.beatId ?? null,
31858
+ code: "FFMPEG_MISSING",
31859
+ error: "FFmpeg (ffmpeg/ffprobe) was not found on PATH; rendering needs it for audio mux and duration checks. Install it (macOS: `brew install ffmpeg`) and retry."
31860
+ };
31861
+ }
31862
+ let autoSyncApplied = false;
31863
+ let autoSyncWarning;
31864
+ if (!opts.beatId && existsSync15(join14(projectDir, "STORYBOARD.md"))) {
31865
+ try {
31866
+ const rootPlan = await createProjectRootSyncPlan({ projectDir });
31867
+ const subPlans = await createSubCompDurationSyncPlans({
31868
+ projectDir,
31869
+ beats: await loadProjectRootSyncBeats(projectDir)
31870
+ });
31871
+ if (rootPlan.changed || subPlans.some((plan) => plan.changed)) {
31872
+ await executeSceneRepair({ projectDir });
31873
+ autoSyncApplied = true;
31874
+ autoSyncWarning = "Narration-synced durations drifted from the composed timeline; root and scene durations were re-synced before rendering (same pass as `vibe build --stage sync`).";
31875
+ }
31876
+ } catch (error) {
31877
+ autoSyncWarning = `Narration drift check skipped: ${error instanceof Error ? error.message : String(error)}`;
31878
+ }
31879
+ }
31836
31880
  let root = opts.root ?? projectConfig.config.composition.entry;
31837
31881
  if (opts.beatId) {
31838
31882
  const prepared = await prepareBeatRenderRoot({
@@ -31949,7 +31993,9 @@ async function executeSceneRender(opts = {}) {
31949
31993
  format: config2.format,
31950
31994
  audioCount,
31951
31995
  audioMuxApplied,
31952
- audioMuxWarning
31996
+ audioMuxWarning,
31997
+ ...autoSyncApplied ? { autoSyncApplied } : {},
31998
+ ...autoSyncWarning ? { autoSyncWarning } : {}
31953
31999
  };
31954
32000
  if (opts.revealInFinder) {
31955
32001
  try {
@@ -32209,6 +32255,10 @@ var init_scene_render = __esm({
32209
32255
  "src/commands/_shared/scene-render.ts"() {
32210
32256
  "use strict";
32211
32257
  init_chrome();
32258
+ init_ffmpeg_gate();
32259
+ init_root_sync();
32260
+ init_sub_comp_duration_sync();
32261
+ init_scene_repair();
32212
32262
  init_scene_lint();
32213
32263
  init_scene_audio_scan();
32214
32264
  init_scene_audio_mux();
@@ -33659,6 +33709,28 @@ async function executeSceneBuild(opts) {
33659
33709
  providerResolution: buildPlan.providerResolution,
33660
33710
  ...result
33661
33711
  });
33712
+ if (shouldRunStage(selectedStage, "assets") || shouldRunStage(selectedStage, "sync")) {
33713
+ if (!ffmpegToolsAvailable()) {
33714
+ return finishBuildResult({
33715
+ success: false,
33716
+ phase: "failed",
33717
+ mode,
33718
+ selectedStage,
33719
+ code: "FFMPEG_MISSING",
33720
+ error: "ffprobe (part of FFmpeg) was not found on PATH. Narration-synced durations cannot be computed without it.",
33721
+ message: "ffprobe not found on PATH.",
33722
+ suggestion: "Install FFmpeg (macOS: `brew install ffmpeg`), then re-run the build.",
33723
+ recoverable: true,
33724
+ beats: [],
33725
+ stageReports,
33726
+ warnings,
33727
+ retryWith,
33728
+ status: "failed",
33729
+ currentStage: "assets",
33730
+ totalLatencyMs: Date.now() - startedAt
33731
+ });
33732
+ }
33733
+ }
33662
33734
  if (!buildPlan.validation.ok) {
33663
33735
  let invalidBeats = [];
33664
33736
  if (existsSync19(storyboardPath)) {
@@ -35330,6 +35402,7 @@ var init_scene_build = __esm({
35330
35402
  import_dotenv3 = __toESM(require_main(), 1);
35331
35403
  init_dist2();
35332
35404
  init_audio();
35405
+ init_ffmpeg_gate();
35333
35406
  init_concurrency();
35334
35407
  init_compose_scenes_skills();
35335
35408
  init_compose_prompts();
@@ -53467,10 +53540,13 @@ function mapRenderResultToToolResult(result, projectDir) {
53467
53540
  format: result.format,
53468
53541
  audioCount: result.audioCount,
53469
53542
  audioMuxApplied: result.audioMuxApplied,
53470
- audioMuxWarning: result.audioMuxWarning
53543
+ audioMuxWarning: result.audioMuxWarning,
53544
+ ...result.autoSyncApplied ? { autoSyncApplied: true } : {},
53545
+ ...result.autoSyncWarning ? { autoSyncWarning: result.autoSyncWarning } : {}
53471
53546
  },
53472
53547
  humanLines: [
53473
53548
  `\u2705 Render complete: ${result.absoluteOutputPath ?? result.outputPath}`,
53549
+ ...result.autoSyncApplied ? [` auto-sync: narration-synced durations were re-applied before render`] : [],
53474
53550
  ` duration: ${((result.durationMs ?? 0) / 1e3).toFixed(1)}s`,
53475
53551
  ` frames: ${result.framesRendered ?? "?"}${result.totalFrames ? ` / ${result.totalFrames}` : ""}`,
53476
53552
  ` config: ${result.fps}fps \xB7 ${result.quality} \xB7 ${result.format}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.111.0",
3
+ "version": "0.111.3",
4
4
  "description": "VibeFrame CLI - agentic video workflow runtime",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -83,8 +83,8 @@
83
83
  "tsx": "^4.21.0",
84
84
  "typescript": "^5.3.3",
85
85
  "vitest": "^1.2.2",
86
- "@vibeframe/core": "0.111.0",
87
- "@vibeframe/ai-providers": "0.111.0"
86
+ "@vibeframe/ai-providers": "0.111.3",
87
+ "@vibeframe/core": "0.111.3"
88
88
  },
89
89
  "engines": {
90
90
  "node": ">=20"