@vibeframe/mcp-server 0.74.0 → 0.75.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 (2) hide show
  1. package/dist/index.js +924 -1361
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -7620,9 +7620,6 @@ tmp/
7620
7620
  *.log
7621
7621
  `;
7622
7622
  }
7623
- function isSceneScaffoldProfile(value) {
7624
- return value === "minimal" || value === "agent" || value === "full";
7625
- }
7626
7623
  function describeSceneScaffold(opts) {
7627
7624
  const dir = resolve(opts.dir);
7628
7625
  const profile = opts.profile ?? "full";
@@ -261959,9 +261956,9 @@ ${lanes.join("\n")}
261959
261956
  /*ignoreCase*/
261960
261957
  false
261961
261958
  )) {
261962
- const basename18 = getBaseFileName(a.fileName);
261963
- if (basename18 === "lib.d.ts" || basename18 === "lib.es6.d.ts") return 0;
261964
- const name = removeSuffix(removePrefix(basename18, "lib."), ".d.ts");
261959
+ const basename17 = getBaseFileName(a.fileName);
261960
+ if (basename17 === "lib.d.ts" || basename17 === "lib.es6.d.ts") return 0;
261961
+ const name = removeSuffix(removePrefix(basename17, "lib."), ".d.ts");
261965
261962
  const index = libs.indexOf(name);
261966
261963
  if (index !== -1) return index + 1;
261967
261964
  }
@@ -325707,8 +325704,8 @@ ${options.prefix}` : "\n" : options.prefix
325707
325704
  }
325708
325705
  };
325709
325706
  for (const file of files) {
325710
- const basename18 = getBaseFileName(file);
325711
- if (basename18 === "package.json" || basename18 === "bower.json") {
325707
+ const basename17 = getBaseFileName(file);
325708
+ if (basename17 === "package.json" || basename17 === "bower.json") {
325712
325709
  createProjectWatcher(
325713
325710
  file,
325714
325711
  "FileWatcher"
@@ -329380,8 +329377,8 @@ All files are: ${JSON.stringify(names)}`,
329380
329377
  var _a7;
329381
329378
  const fileOrDirectoryPath = removeIgnoredPath(this.toPath(fileOrDirectory));
329382
329379
  if (!fileOrDirectoryPath) return;
329383
- const basename18 = getBaseFileName(fileOrDirectoryPath);
329384
- if (((_a7 = result.affectedModuleSpecifierCacheProjects) == null ? void 0 : _a7.size) && (basename18 === "package.json" || basename18 === "node_modules")) {
329380
+ const basename17 = getBaseFileName(fileOrDirectoryPath);
329381
+ if (((_a7 = result.affectedModuleSpecifierCacheProjects) == null ? void 0 : _a7.size) && (basename17 === "package.json" || basename17 === "node_modules")) {
329385
329382
  result.affectedModuleSpecifierCacheProjects.forEach((project) => {
329386
329383
  var _a23;
329387
329384
  (_a23 = project.getModuleSpecifierCache()) == null ? void 0 : _a23.clear();
@@ -447058,152 +447055,266 @@ var init_scene_lint = __esm({
447058
447055
  }
447059
447056
  });
447060
447057
 
447061
- // ../cli/src/pipeline/renderers/chrome.ts
447062
- import { existsSync as existsSync19 } from "node:fs";
447063
- import { homedir as homedir4 } from "node:os";
447064
- import { join as join19 } from "node:path";
447065
- function findChrome() {
447066
- const candidates = [
447067
- process.env.HYPERFRAMES_CHROME_PATH,
447068
- process.env.CHROME_PATH,
447069
- // puppeteer auto-downloaded headless shell
447070
- join19(
447071
- homedir4(),
447072
- ".cache",
447073
- "puppeteer",
447074
- "chrome-headless-shell",
447075
- "mac_arm-147.0.7727.56",
447076
- "chrome-headless-shell-mac_arm",
447077
- "chrome-headless-shell"
447078
- ),
447079
- // system Chrome / Chromium
447080
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
447081
- "/Applications/Chromium.app/Contents/MacOS/Chromium",
447082
- "/usr/bin/google-chrome",
447083
- "/usr/bin/chromium",
447084
- "/usr/bin/chromium-browser",
447085
- "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
447086
- ];
447087
- for (const c of candidates) {
447088
- if (c && existsSync19(c)) return c;
447089
- }
447090
- return void 0;
447058
+ // ../cli/src/commands/output.ts
447059
+ var output_exports = {};
447060
+ __export(output_exports, {
447061
+ COST_ESTIMATES: () => COST_ESTIMATES,
447062
+ ExitCode: () => ExitCode,
447063
+ _resetDeprecationMemoryForTesting: () => _resetDeprecationMemoryForTesting,
447064
+ apiError: () => apiError,
447065
+ authError: () => authError,
447066
+ emitDeprecationWarning: () => emitDeprecationWarning,
447067
+ exitWithError: () => exitWithError,
447068
+ generalError: () => generalError,
447069
+ isJsonMode: () => isJsonMode,
447070
+ isQuietMode: () => isQuietMode,
447071
+ log: () => log,
447072
+ networkError: () => networkError,
447073
+ notFoundError: () => notFoundError,
447074
+ outputError: () => outputError,
447075
+ outputResult: () => outputResult,
447076
+ outputSuccess: () => outputSuccess,
447077
+ spinner: () => spinner,
447078
+ suggestNext: () => suggestNext,
447079
+ usageError: () => usageError
447080
+ });
447081
+ function usageError(msg, suggestion) {
447082
+ return { success: false, error: msg, code: "USAGE_ERROR", exitCode: 2 /* USAGE */, suggestion, retryable: false };
447091
447083
  }
447092
- async function preflightChrome() {
447093
- return findChrome() ? { ok: true } : { ok: false, reason: CHROME_NOT_FOUND_REASON };
447084
+ function authError(envVar, provider) {
447085
+ return {
447086
+ success: false,
447087
+ error: `${provider} API key required.`,
447088
+ code: "API_KEY_MISSING",
447089
+ exitCode: 4 /* AUTH */,
447090
+ suggestion: `Set ${envVar} in .env, or run: vibe setup`,
447091
+ retryable: false
447092
+ };
447094
447093
  }
447095
- var CHROME_NOT_FOUND_REASON;
447096
- var init_chrome3 = __esm({
447097
- "../cli/src/pipeline/renderers/chrome.ts"() {
447098
- "use strict";
447099
- CHROME_NOT_FOUND_REASON = "Chrome not found. Set HYPERFRAMES_CHROME_PATH, or install Chrome (macOS: brew install --cask google-chrome \xB7 Linux: apt install chromium). Run `vibe doctor` for details.";
447100
- }
447101
- });
447102
-
447103
- // ../cli/src/commands/_shared/scene-audio-scan.ts
447104
- import { readFile as readFile5 } from "node:fs/promises";
447105
- import { resolve as resolve14 } from "node:path";
447106
- function parseRootClips(rootHtml) {
447107
- const clips = [];
447108
- const clipRegex = /<div\b[^>]*class="clip"[^>]*>/gi;
447109
- let match2;
447110
- while ((match2 = clipRegex.exec(rootHtml)) !== null) {
447111
- const tag = match2[0];
447112
- const compositionId = pickAttr(tag, "data-composition-id");
447113
- const compositionSrc = pickAttr(tag, "data-composition-src");
447114
- const start = pickNumberAttr(tag, "data-start");
447115
- const duration = pickNumberAttr(tag, "data-duration");
447116
- const trackIndex = pickNumberAttr(tag, "data-track-index") ?? 1;
447117
- if (!compositionId || !compositionSrc || start === null || duration === null) {
447118
- continue;
447094
+ function apiError(msg, retryable = false) {
447095
+ for (const hint of PROVIDER_ERROR_HINTS) {
447096
+ if (hint.pattern.test(msg)) {
447097
+ return { success: false, error: msg, code: "API_ERROR", exitCode: 5 /* API_ERROR */, suggestion: hint.suggestion, retryable: hint.retryable };
447119
447098
  }
447120
- clips.push({ compositionId, compositionSrc, start, duration, trackIndex });
447121
447099
  }
447122
- return clips;
447100
+ return { success: false, error: msg, code: "API_ERROR", exitCode: 5 /* API_ERROR */, suggestion: retryable ? "Retry the command." : void 0, retryable };
447123
447101
  }
447124
- function parseSceneAudios(compositionHtml) {
447125
- const out = [];
447126
- const audioRegex = /<audio\b([^>]*)>/gi;
447127
- let match2;
447128
- while ((match2 = audioRegex.exec(compositionHtml)) !== null) {
447129
- const attrs = match2[1];
447130
- const src = pickAttr(attrs, "src");
447131
- if (!src) continue;
447132
- const localStart = pickNumberAttr(attrs, "data-start") ?? 0;
447133
- const durationRaw = pickAttr(attrs, "data-duration");
447134
- const durationHint = !durationRaw || durationRaw === "auto" ? "auto" : Number(durationRaw);
447135
- const volume = pickNumberAttr(attrs, "data-volume") ?? 1;
447136
- const trackIndex = pickNumberAttr(attrs, "data-track-index") ?? 2;
447137
- out.push({ srcRel: src, localStart, durationHint, volume, trackIndex });
447138
- }
447139
- return out;
447102
+ function notFoundError(path14) {
447103
+ return { success: false, error: `File not found: ${path14}`, code: "NOT_FOUND", exitCode: 3 /* NOT_FOUND */, retryable: false };
447140
447104
  }
447141
- function makeFsCompositionReader(projectDir) {
447142
- return async (compositionSrcRel) => {
447143
- const abs = resolve14(projectDir, compositionSrcRel);
447144
- try {
447145
- return await readFile5(abs, "utf-8");
447146
- } catch {
447147
- return null;
447105
+ function networkError(msg) {
447106
+ return { success: false, error: msg, code: "NETWORK_ERROR", exitCode: 6 /* NETWORK */, suggestion: "Check your internet connection and retry.", retryable: true };
447107
+ }
447108
+ function generalError(msg, suggestion) {
447109
+ return { success: false, error: msg, code: "ERROR", exitCode: 1 /* GENERAL */, suggestion, retryable: false };
447110
+ }
447111
+ function exitWithError(err) {
447112
+ if (isJsonMode()) {
447113
+ console.error(JSON.stringify(err, null, 2));
447114
+ } else {
447115
+ console.error(source_default.red(`
447116
+ ${err.error}`));
447117
+ if (err.suggestion) {
447118
+ console.error(source_default.dim(` ${err.suggestion}`));
447148
447119
  }
447120
+ console.error();
447121
+ }
447122
+ process.exit(err.exitCode);
447123
+ }
447124
+ function isJsonMode() {
447125
+ return process.env.VIBE_JSON_OUTPUT === "1";
447126
+ }
447127
+ function isQuietMode() {
447128
+ return process.env.VIBE_QUIET_OUTPUT === "1";
447129
+ }
447130
+ function formatCost(min, max, unit) {
447131
+ if (min === 0 && max === 0) return "Free";
447132
+ if (min === max) return `~$${min.toFixed(2)} ${unit}`;
447133
+ return `~$${min.toFixed(2)}-$${max.toFixed(2)} ${unit}`;
447134
+ }
447135
+ function lookupCostEstimateUpperBound(command3) {
447136
+ return COST_ESTIMATES[command3]?.max ?? 0;
447137
+ }
447138
+ function outputSuccess(opts) {
447139
+ const elapsedMs = Math.max(0, Date.now() - opts.startedAt);
447140
+ const costUsd = opts.costUsd ?? (opts.dryRun ? lookupCostEstimateUpperBound(opts.command) : 0);
447141
+ const envelope = {
447142
+ command: opts.command,
447143
+ ...opts.dryRun ? { dryRun: true } : {},
447144
+ elapsedMs,
447145
+ costUsd,
447146
+ warnings: opts.warnings ?? [],
447147
+ data: opts.data
447149
447148
  };
447149
+ if (isJsonMode()) {
447150
+ const fields = process.env.VIBE_OUTPUT_FIELDS;
447151
+ if (fields) {
447152
+ const keys2 = fields.split(",").map((k) => k.trim());
447153
+ const data = opts.data;
447154
+ const filteredData = {};
447155
+ for (const key2 of keys2) {
447156
+ if (key2 in data) filteredData[key2] = data[key2];
447157
+ }
447158
+ envelope.data = filteredData;
447159
+ }
447160
+ console.log(JSON.stringify(envelope, null, 2));
447161
+ return;
447162
+ }
447163
+ if (isQuietMode()) {
447164
+ const data = opts.data;
447165
+ const primary = data.outputPath ?? data.output ?? data.path ?? data.url ?? data.id;
447166
+ if (primary !== void 0) console.log(String(primary));
447167
+ }
447150
447168
  }
447151
- async function scanSceneAudio(opts) {
447152
- const reader = opts.readComposition ?? makeFsCompositionReader(opts.projectDir);
447153
- const clips = parseRootClips(opts.rootHtml);
447154
- const out = [];
447155
- for (const clip of clips) {
447156
- const html = await reader(clip.compositionSrc);
447157
- if (!html) continue;
447158
- const audios = parseSceneAudios(html);
447159
- for (const audio of audios) {
447160
- out.push({
447161
- srcRel: audio.srcRel,
447162
- srcAbs: resolve14(opts.projectDir, audio.srcRel),
447163
- absoluteStart: clip.start + audio.localStart,
447164
- durationHint: audio.durationHint,
447165
- clipDurationCap: clip.duration - audio.localStart,
447166
- volume: audio.volume,
447167
- trackIndex: audio.trackIndex,
447168
- compositionSrc: clip.compositionSrc
447169
- });
447169
+ function outputResult(result) {
447170
+ if (result.dryRun && result.command && typeof result.command === "string") {
447171
+ const cost = COST_ESTIMATES[result.command];
447172
+ if (cost) {
447173
+ result.estimatedCost = formatCost(cost.min, cost.max, cost.unit);
447170
447174
  }
447171
447175
  }
447172
- const maxClipEnd = clips.reduce((m, c) => Math.max(m, c.start + c.duration), 0);
447173
- const rootAudios = parseSceneAudios(opts.rootHtml);
447174
- for (const audio of rootAudios) {
447175
- out.push({
447176
- srcRel: audio.srcRel,
447177
- srcAbs: resolve14(opts.projectDir, audio.srcRel),
447178
- absoluteStart: audio.localStart,
447179
- durationHint: audio.durationHint,
447180
- clipDurationCap: Math.max(0, maxClipEnd - audio.localStart),
447181
- volume: audio.volume,
447182
- trackIndex: audio.trackIndex,
447183
- compositionSrc: "(root)"
447184
- });
447176
+ if (isJsonMode()) {
447177
+ const fields = process.env.VIBE_OUTPUT_FIELDS;
447178
+ if (fields) {
447179
+ const keys2 = fields.split(",").map((k) => k.trim());
447180
+ const filtered = {};
447181
+ for (const key2 of keys2) {
447182
+ if (key2 in result) filtered[key2] = result[key2];
447183
+ }
447184
+ if ("success" in result) filtered.success = result.success;
447185
+ console.log(JSON.stringify(filtered, null, 2));
447186
+ } else {
447187
+ console.log(JSON.stringify(result, null, 2));
447188
+ }
447189
+ } else if (isQuietMode()) {
447190
+ const primary = result.output ?? result.path ?? result.url ?? result.id ?? result.result;
447191
+ if (primary !== void 0) console.log(String(primary));
447185
447192
  }
447186
- out.sort((a, b) => a.absoluteStart - b.absoluteStart);
447187
- return out;
447188
447193
  }
447189
- function pickAttr(tag, name) {
447190
- const re = new RegExp(`\\b${escapeRegex3(name)}\\s*=\\s*("([^"]*)"|'([^']*)')`);
447191
- const m = tag.match(re);
447192
- if (!m) return null;
447193
- return m[2] ?? m[3] ?? null;
447194
+ function log(...args) {
447195
+ if (!isJsonMode() && !isQuietMode()) {
447196
+ console.log(...args);
447197
+ }
447194
447198
  }
447195
- function pickNumberAttr(tag, name) {
447196
- const raw2 = pickAttr(tag, name);
447197
- if (raw2 === null) return null;
447198
- const n = Number(raw2);
447199
- return Number.isFinite(n) ? n : null;
447199
+ function spinner(text) {
447200
+ if (isJsonMode() || isQuietMode()) {
447201
+ return ora({ text, isSilent: true });
447202
+ }
447203
+ return ora(text);
447200
447204
  }
447201
- function escapeRegex3(s) {
447202
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
447205
+ function suggestNext(tip) {
447206
+ if (!isJsonMode() && !isQuietMode() && process.stdout.isTTY) {
447207
+ console.log(source_default.dim(`
447208
+ Tip: ${tip}`));
447209
+ }
447203
447210
  }
447204
- var init_scene_audio_scan = __esm({
447205
- "../cli/src/commands/_shared/scene-audio-scan.ts"() {
447211
+ function emitDeprecationWarning(oldName, newName, removeIn) {
447212
+ if (isJsonMode() || isQuietMode()) return;
447213
+ if (!process.stderr.isTTY) return;
447214
+ const key2 = `${oldName}\u2192${newName}`;
447215
+ if (_seenDeprecations.has(key2)) return;
447216
+ _seenDeprecations.add(key2);
447217
+ process.stderr.write(
447218
+ source_default.yellow(`[deprecated] '${oldName}' is deprecated; use '${newName}' instead. Alias will be removed in ${removeIn}.`) + "\n"
447219
+ );
447220
+ }
447221
+ function _resetDeprecationMemoryForTesting() {
447222
+ _seenDeprecations.clear();
447223
+ }
447224
+ function outputError(error, details) {
447225
+ if (isJsonMode()) {
447226
+ console.error(JSON.stringify({ success: false, error, ...details }, null, 2));
447227
+ } else {
447228
+ console.error(error);
447229
+ }
447230
+ }
447231
+ var ExitCode, PROVIDER_ERROR_HINTS, COST_ESTIMATES, _seenDeprecations;
447232
+ var init_output = __esm({
447233
+ "../cli/src/commands/output.ts"() {
447206
447234
  "use strict";
447235
+ init_source();
447236
+ init_ora();
447237
+ ExitCode = /* @__PURE__ */ ((ExitCode2) => {
447238
+ ExitCode2[ExitCode2["SUCCESS"] = 0] = "SUCCESS";
447239
+ ExitCode2[ExitCode2["GENERAL"] = 1] = "GENERAL";
447240
+ ExitCode2[ExitCode2["USAGE"] = 2] = "USAGE";
447241
+ ExitCode2[ExitCode2["NOT_FOUND"] = 3] = "NOT_FOUND";
447242
+ ExitCode2[ExitCode2["AUTH"] = 4] = "AUTH";
447243
+ ExitCode2[ExitCode2["API_ERROR"] = 5] = "API_ERROR";
447244
+ ExitCode2[ExitCode2["NETWORK"] = 6] = "NETWORK";
447245
+ return ExitCode2;
447246
+ })(ExitCode || {});
447247
+ PROVIDER_ERROR_HINTS = [
447248
+ // Billing (must precede the 429 rate-limit pattern)
447249
+ { pattern: /402|payment.*required|billing|INSUFFICIENT_BALANCE|insufficient.*(credit|funds|balance)|balance.*(not.*enough|insufficient)|credits?.*exhausted|account.*balance/i, suggestion: "Account balance or credits exhausted. Top up at the provider dashboard, or try -p <other-provider>.", retryable: false },
447250
+ // Rate limits / quota
447251
+ { pattern: /429|rate.?limit|too many requests/i, suggestion: "Rate limited. Wait 30-60 seconds and retry, or check your plan's rate limits.", retryable: true },
447252
+ { pattern: /RESOURCE_EXHAUSTED|quota.*exceeded|requests.*per.*(minute|day)/i, suggestion: "Quota exceeded. Wait for the quota window to reset, or upgrade your plan. Consider -p <other-provider> to use a different provider.", retryable: true },
447253
+ // Auth
447254
+ { pattern: /401|unauthorized|(invalid|incorrect).*api.?key|invalid_api_key|authentication.*(failed|error)|missing.*api.?key|did not start with 'key_'/i, suggestion: "API key is invalid or expired. Run 'vibe setup' to update, or check the key at the provider's dashboard.", retryable: false },
447255
+ { pattern: /403|forbidden|permission.*denied/i, suggestion: "Access denied. Your API key may lack required permissions, or the feature requires a paid plan.", retryable: false },
447256
+ // Server
447257
+ { pattern: /500|internal.*error|server.*error/i, suggestion: "Provider server error. Retry in a few minutes.", retryable: true },
447258
+ { pattern: /503|service.*unavailable|overloaded|overloaded_error/i, suggestion: "Provider is temporarily overloaded. Retry in 1-2 minutes, or switch provider with -p.", retryable: true },
447259
+ { pattern: /timeout|timed?\s*out|ETIMEDOUT|ECONNRESET|socket.*hang.?up/i, suggestion: "Request timed out. The provider may be slow. Retry, or try a different provider with -p flag.", retryable: true },
447260
+ // Content policy
447261
+ { pattern: /content.*(policy|filter)|safety|moderation|blocked.*(by|due)|content_policy_violation|restricted.*content/i, suggestion: "Content was blocked by the provider's safety filter. Rephrase your prompt to avoid sensitive terms.", retryable: false },
447262
+ // Model / context
447263
+ { pattern: /context_length_exceeded|maximum.*context.*length|token.*limit.*exceeded|prompt.*too.*long/i, suggestion: "Input exceeds the model's context window. Shorten the prompt, or use a model with larger context (run 'vibe schema <command>' for options).", retryable: false },
447264
+ { pattern: /model.*not.*found|invalid.*model|unknown.*model|model_not_found/i, suggestion: "The specified model is unavailable. Check 'vibe schema <command>' for valid model options.", retryable: false },
447265
+ // Provider-specific
447266
+ { pattern: /voice.*not.*found|voice_not_found|invalid.*voice.?id/i, suggestion: "Voice ID not found. Run 'vibe audio list-voices' to list available voices, then pass --voice <id>.", retryable: false },
447267
+ { pattern: /character.*(count|limit).*exceeded|invalid_character_count/i, suggestion: "Text exceeds the TTS provider's character limit. Shorten the text or split into chunks.", retryable: false },
447268
+ { pattern: /invalid.*aspect.*ratio|unsupported.*aspect.*ratio|unsupported.*resolution/i, suggestion: "This aspect ratio or resolution isn't supported by the chosen model. Check 'vibe schema <command>' for supported values.", retryable: false },
447269
+ { pattern: /invalid.*file.*format|unsupported.*(format|codec)|unsupported.*media.?type/i, suggestion: "Input file format not supported. Convert to MP4/MP3/PNG first with 'vibe export' or 'ffmpeg'.", retryable: false },
447270
+ { pattern: /region.*(restriction|not.*supported|unavailable)|geo.?blocked/i, suggestion: "Provider unavailable in your region. Try -p <other-provider>, or use a supported region.", retryable: false },
447271
+ { pattern: /task.*(not.*found|expired)|job.*(not.*found|expired)/i, suggestion: "The async task expired or was never created. Re-run the command to start a new task.", retryable: true }
447272
+ ];
447273
+ COST_ESTIMATES = {
447274
+ // Free
447275
+ "detect scenes": { min: 0, max: 0, unit: "free" },
447276
+ "detect silence": { min: 0, max: 0, unit: "free" },
447277
+ "detect beats": { min: 0, max: 0, unit: "free" },
447278
+ "edit silence-cut": { min: 0, max: 0, unit: "free" },
447279
+ "edit fade": { min: 0, max: 0, unit: "free" },
447280
+ "edit noise-reduce": { min: 0, max: 0, unit: "free" },
447281
+ "edit reframe": { min: 0, max: 0, unit: "free" },
447282
+ "edit interpolate": { min: 0, max: 0, unit: "free" },
447283
+ "edit upscale-video": { min: 0, max: 0, unit: "free" },
447284
+ // Low
447285
+ "analyze media": { min: 0.01, max: 0.05, unit: "per call" },
447286
+ "analyze video": { min: 0.01, max: 0.1, unit: "per video" },
447287
+ "analyze review": { min: 0.01, max: 0.1, unit: "per video" },
447288
+ "generate image": { min: 0.01, max: 0.07, unit: "per image" },
447289
+ "generate thumbnail": { min: 0.01, max: 0.05, unit: "per image" },
447290
+ "generate storyboard": { min: 0.01, max: 0.05, unit: "per call" },
447291
+ "ai transcribe": { min: 0.01, max: 0.1, unit: "per minute" },
447292
+ "audio transcribe": { min: 0.01, max: 0.1, unit: "per minute" },
447293
+ "edit caption": { min: 0.01, max: 0.1, unit: "per video" },
447294
+ "edit jump-cut": { min: 0.01, max: 0.1, unit: "per video" },
447295
+ "edit translate-srt": { min: 0.01, max: 0.05, unit: "per file" },
447296
+ "edit animated-caption": { min: 0.01, max: 0.1, unit: "per video" },
447297
+ // Medium
447298
+ "generate speech": { min: 0.05, max: 0.3, unit: "per request" },
447299
+ "generate sound-effect": { min: 0.05, max: 0.2, unit: "per request" },
447300
+ "generate music": { min: 0.05, max: 0.5, unit: "per request" },
447301
+ "generate motion": { min: 0.01, max: 0.1, unit: "per generation" },
447302
+ "edit grade": { min: 0.01, max: 0.05, unit: "per video" },
447303
+ "edit speed-ramp": { min: 0.05, max: 0.15, unit: "per video" },
447304
+ "edit text-overlay": { min: 0, max: 0.05, unit: "per video" },
447305
+ // High
447306
+ "generate video": { min: 0.5, max: 5, unit: "per video" },
447307
+ "edit image": { min: 0.05, max: 0.5, unit: "per edit" },
447308
+ // Very High
447309
+ "pipeline highlights": { min: 0.05, max: 1, unit: "per analysis" },
447310
+ "pipeline auto-shorts": { min: 0.1, max: 2, unit: "per batch" },
447311
+ "pipeline animated-caption": { min: 0.01, max: 0.1, unit: "per video" },
447312
+ "pipeline regenerate-scene": { min: 0.5, max: 5, unit: "per scene" },
447313
+ // Scene composition (v0.59+) — per-beat composer call ≈ $0.06 (PR #111
447314
+ // pre-flight). Range covers a 1-beat preview to a 10-beat long-form.
447315
+ "compose scenes with skills": { min: 0.05, max: 1.5, unit: "per pipeline" }
447316
+ };
447317
+ _seenDeprecations = /* @__PURE__ */ new Set();
447207
447318
  }
447208
447319
  });
447209
447320
 
@@ -447277,278 +447388,6 @@ var init_exec_safe = __esm({
447277
447388
  }
447278
447389
  });
447279
447390
 
447280
- // ../cli/src/commands/_shared/scene-audio-mux.ts
447281
- import { rename as rename3, unlink as unlink4 } from "node:fs/promises";
447282
- import { resolve as resolve15, dirname as dirname12, extname as extname4, basename as basename4 } from "node:path";
447283
- function buildAudioMuxFilter(audios) {
447284
- if (audios.length === 0) return null;
447285
- const labels = [];
447286
- const stages = [];
447287
- audios.forEach((a, i) => {
447288
- const inputIdx = i + 1;
447289
- const delayMs = Math.max(0, Math.round(a.absoluteStart * 1e3));
447290
- const volume = Number.isFinite(a.volume) ? a.volume : 1;
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);
447294
- const label = `a${i}`;
447295
- const stage = [
447296
- `[${inputIdx}:a]`,
447297
- `atrim=duration=${trimSec.toFixed(3)},`,
447298
- `asetpts=PTS-STARTPTS,`,
447299
- `adelay=${delayMs}:all=1,`,
447300
- `volume=${volume}`,
447301
- `[${label}]`
447302
- ].join("");
447303
- stages.push(stage);
447304
- labels.push(`[${label}]`);
447305
- });
447306
- if (audios.length === 1) {
447307
- return {
447308
- filterComplex: stages.join(";"),
447309
- outLabel: labels[0],
447310
- inputCount: 1
447311
- };
447312
- }
447313
- const mix = `${labels.join("")}amix=inputs=${audios.length}:dropout_transition=0:normalize=0[mixed]`;
447314
- return {
447315
- filterComplex: `${stages.join(";")};${mix}`,
447316
- outLabel: "[mixed]",
447317
- inputCount: audios.length
447318
- };
447319
- }
447320
- function audioCodecForFormat(format4) {
447321
- if (format4 === "webm") return "libopus";
447322
- if (format4 === "mov") return "pcm_s16le";
447323
- return "aac";
447324
- }
447325
- async function muxAudioIntoVideo(opts) {
447326
- if (opts.audios.length === 0) {
447327
- return { success: true, outputPath: opts.videoPath, audioCount: 0 };
447328
- }
447329
- if (!commandExists("ffmpeg")) {
447330
- return {
447331
- success: false,
447332
- outputPath: opts.videoPath,
447333
- audioCount: opts.audios.length,
447334
- error: "ffmpeg not found in PATH \u2014 install via `brew install ffmpeg` (mac) or your package manager"
447335
- };
447336
- }
447337
- const filter4 = buildAudioMuxFilter(opts.audios);
447338
- if (!filter4) {
447339
- return { success: true, outputPath: opts.videoPath, audioCount: 0 };
447340
- }
447341
- const ext = extname4(opts.videoPath) || `.${opts.format}`;
447342
- const tmpPath = resolve15(
447343
- dirname12(opts.videoPath),
447344
- `.${basename4(opts.videoPath, ext)}.muxing${ext}`
447345
- );
447346
- const args = ["-y", "-loglevel", "error", "-i", opts.videoPath];
447347
- for (const a of opts.audios) {
447348
- args.push("-i", a.srcAbs);
447349
- }
447350
- args.push(
447351
- "-filter_complex",
447352
- filter4.filterComplex,
447353
- "-map",
447354
- "0:v",
447355
- "-map",
447356
- filter4.outLabel,
447357
- "-c:v",
447358
- "copy",
447359
- "-c:a",
447360
- audioCodecForFormat(opts.format),
447361
- // Cap on the video duration so audio that overruns the producer's render
447362
- // (e.g. a long Kokoro wav on a short scene) doesn't extend the output.
447363
- // Video drives the timeline because the producer already counted frames.
447364
- "-t",
447365
- opts.totalDuration && opts.totalDuration > 0 ? opts.totalDuration.toFixed(3) : opts.videoDuration?.toFixed(3) ?? ""
447366
- );
447367
- if (args[args.length - 1] === "") {
447368
- args.pop();
447369
- args.pop();
447370
- }
447371
- args.push("-movflags", "+faststart", tmpPath);
447372
- try {
447373
- const { stderr } = await execSafe("ffmpeg", args);
447374
- if (stderr && opts.onProgress) {
447375
- stderr.split(/\r?\n/).forEach((line) => opts.onProgress?.(line));
447376
- }
447377
- } catch (err) {
447378
- const msg = err instanceof Error ? err.message : String(err);
447379
- try {
447380
- await unlink4(tmpPath);
447381
- } catch {
447382
- }
447383
- return {
447384
- success: false,
447385
- outputPath: opts.videoPath,
447386
- audioCount: opts.audios.length,
447387
- error: `ffmpeg mux failed: ${msg}`
447388
- };
447389
- }
447390
- await rename3(tmpPath, opts.videoPath);
447391
- return {
447392
- success: true,
447393
- outputPath: opts.videoPath,
447394
- audioCount: opts.audios.length
447395
- };
447396
- }
447397
- var init_scene_audio_mux = __esm({
447398
- "../cli/src/commands/_shared/scene-audio-mux.ts"() {
447399
- "use strict";
447400
- init_exec_safe();
447401
- }
447402
- });
447403
-
447404
- // ../cli/src/commands/_shared/scene-render.ts
447405
- var scene_render_exports = {};
447406
- __export(scene_render_exports, {
447407
- buildRenderConfig: () => buildRenderConfig,
447408
- defaultOutputPath: () => defaultOutputPath,
447409
- executeSceneRender: () => executeSceneRender,
447410
- qualityToCrf: () => qualityToCrf
447411
- });
447412
- import { mkdir as mkdir6, readFile as readFile6, stat as stat2 } from "node:fs/promises";
447413
- import { existsSync as existsSync20 } from "node:fs";
447414
- import { resolve as resolve16, relative as relative4, dirname as dirname13, basename as basename5 } from "node:path";
447415
- function qualityToCrf(quality = "standard") {
447416
- return quality === "draft" ? 28 : quality === "high" ? 18 : 23;
447417
- }
447418
- function defaultOutputPath(opts) {
447419
- const fmt = opts.format ?? "mp4";
447420
- const now = opts.now ?? /* @__PURE__ */ new Date();
447421
- const stamp = now.toISOString().replace(/[:T]/g, "-").replace(/\..+$/, "");
447422
- const name = (opts.projectName ?? basename5(resolve16(opts.projectDir))) || "scene";
447423
- return resolve16(opts.projectDir, "renders", `${name}-${stamp}.${fmt}`);
447424
- }
447425
- async function readProjectName(projectDir) {
447426
- const cfgPath = resolve16(projectDir, "vibe.project.yaml");
447427
- if (!existsSync20(cfgPath)) return void 0;
447428
- try {
447429
- const raw2 = await (await import("node:fs/promises")).readFile(cfgPath, "utf-8");
447430
- const parsed = (0, import_yaml3.parse)(raw2);
447431
- return parsed?.name ?? void 0;
447432
- } catch {
447433
- return void 0;
447434
- }
447435
- }
447436
- function buildRenderConfig(opts) {
447437
- const quality = opts.quality ?? "standard";
447438
- return {
447439
- fps: opts.fps ?? 30,
447440
- quality,
447441
- format: opts.format ?? "mp4",
447442
- entryFile: opts.entryFile ?? "index.html",
447443
- crf: qualityToCrf(quality),
447444
- workers: opts.workers ?? 1
447445
- };
447446
- }
447447
- async function executeSceneRender(opts = {}) {
447448
- const projectDir = resolve16(opts.projectDir ?? ".");
447449
- const root2 = opts.root ?? "index.html";
447450
- const projectStat = await safeStat(projectDir);
447451
- if (!projectStat || !projectStat.isDirectory()) {
447452
- return { success: false, error: `Project directory not found: ${projectDir}` };
447453
- }
447454
- if (!await rootExists(projectDir, root2)) {
447455
- return {
447456
- success: false,
447457
- error: `Root composition not found: ${resolve16(projectDir, root2)}. Run \`vibe scene init\` first.`
447458
- };
447459
- }
447460
- const chrome2 = await preflightChrome();
447461
- if (!chrome2.ok) {
447462
- return { success: false, error: chrome2.reason };
447463
- }
447464
- const projectName = await readProjectName(projectDir);
447465
- const outputPath = opts.output ? resolve16(projectDir, opts.output) : defaultOutputPath({ projectDir, projectName, format: opts.format });
447466
- await mkdir6(dirname13(outputPath), { recursive: true });
447467
- const config4 = buildRenderConfig({
447468
- fps: opts.fps,
447469
- quality: opts.quality,
447470
- format: opts.format,
447471
- workers: opts.workers,
447472
- entryFile: root2
447473
- });
447474
- const job = createRenderJob(config4);
447475
- const start = Date.now();
447476
- try {
447477
- await executeRenderJob(
447478
- job,
447479
- projectDir,
447480
- outputPath,
447481
- (j, msg) => opts.onProgress?.(j.progress, j.currentStage ?? msg),
447482
- opts.signal
447483
- );
447484
- } catch (err) {
447485
- return {
447486
- success: false,
447487
- error: err instanceof Error ? err.message : String(err)
447488
- };
447489
- }
447490
- let audioCount = 0;
447491
- let audioMuxApplied = false;
447492
- let audioMuxWarning;
447493
- try {
447494
- opts.onProgress?.(0.95, "Mixing audio");
447495
- const rootHtml = await readFile6(resolve16(projectDir, root2), "utf-8");
447496
- const audios = await scanSceneAudio({ projectDir, rootHtml });
447497
- audioCount = audios.length;
447498
- if (audios.length > 0) {
447499
- const videoDuration = job.totalFrames && config4.fps ? job.totalFrames / config4.fps : void 0;
447500
- const mux = await muxAudioIntoVideo({
447501
- videoPath: outputPath,
447502
- audios,
447503
- format: config4.format ?? "mp4",
447504
- videoDuration,
447505
- onProgress: (line) => {
447506
- if (line) opts.onProgress?.(0.97, line);
447507
- }
447508
- });
447509
- if (mux.success) {
447510
- audioMuxApplied = true;
447511
- } else {
447512
- audioMuxWarning = mux.error;
447513
- }
447514
- }
447515
- } catch (err) {
447516
- audioMuxWarning = err instanceof Error ? err.message : String(err);
447517
- }
447518
- return {
447519
- success: true,
447520
- outputPath: relative4(process.cwd(), outputPath) || outputPath,
447521
- durationMs: Date.now() - start,
447522
- framesRendered: job.framesRendered,
447523
- totalFrames: job.totalFrames,
447524
- fps: config4.fps,
447525
- quality: config4.quality,
447526
- format: config4.format,
447527
- audioCount,
447528
- audioMuxApplied,
447529
- audioMuxWarning
447530
- };
447531
- }
447532
- async function safeStat(p) {
447533
- try {
447534
- return await stat2(p);
447535
- } catch {
447536
- return null;
447537
- }
447538
- }
447539
- var import_yaml3;
447540
- var init_scene_render = __esm({
447541
- "../cli/src/commands/_shared/scene-render.ts"() {
447542
- "use strict";
447543
- import_yaml3 = __toESM(require_dist(), 1);
447544
- init_dist2();
447545
- init_chrome3();
447546
- init_scene_lint();
447547
- init_scene_audio_scan();
447548
- init_scene_audio_mux();
447549
- }
447550
- });
447551
-
447552
447391
  // ../cli/src/utils/audio.ts
447553
447392
  async function getAudioDuration(filePath) {
447554
447393
  try {
@@ -447600,6 +447439,126 @@ var init_audio = __esm({
447600
447439
  }
447601
447440
  });
447602
447441
 
447442
+ // ../cli/src/utils/agent-host-detect.ts
447443
+ import { existsSync as existsSync19 } from "node:fs";
447444
+ import { homedir as homedir4 } from "node:os";
447445
+ import { join as join19 } from "node:path";
447446
+ function detectAgentHosts(env4 = process.env) {
447447
+ const home = homedir4();
447448
+ return [
447449
+ {
447450
+ id: "claude-code",
447451
+ label: "Claude Code",
447452
+ detected: false,
447453
+ signals: [],
447454
+ projectFiles: ["CLAUDE.md", ".claude/skills/"]
447455
+ },
447456
+ {
447457
+ id: "codex",
447458
+ label: "Codex (OpenAI)",
447459
+ detected: false,
447460
+ signals: [],
447461
+ projectFiles: ["AGENTS.md"]
447462
+ },
447463
+ {
447464
+ id: "cursor",
447465
+ label: "Cursor",
447466
+ detected: false,
447467
+ signals: [],
447468
+ projectFiles: ["AGENTS.md", ".cursor/rules/"]
447469
+ },
447470
+ {
447471
+ id: "aider",
447472
+ label: "Aider",
447473
+ detected: false,
447474
+ signals: [],
447475
+ projectFiles: ["AGENTS.md", ".aider.conf.yml"]
447476
+ },
447477
+ {
447478
+ id: "gemini-cli",
447479
+ label: "Gemini CLI",
447480
+ detected: false,
447481
+ signals: [],
447482
+ // Per https://geminicli.com/docs/cli/gemini-md/ Gemini CLI's primary
447483
+ // context file is GEMINI.md; AGENTS.md is the cross-tool fallback
447484
+ // VibeFrame writes by default. Both are honoured.
447485
+ projectFiles: ["GEMINI.md", "AGENTS.md", ".gemini/"]
447486
+ },
447487
+ {
447488
+ id: "opencode",
447489
+ label: "OpenCode",
447490
+ detected: false,
447491
+ signals: [],
447492
+ // sst/opencode officially supports the agents.md spec — AGENTS.md at
447493
+ // project root is the standard place. Local config also under
447494
+ // `.opencode/` per https://opencode.ai/docs/config/.
447495
+ projectFiles: ["AGENTS.md", ".opencode/"]
447496
+ }
447497
+ ].map((host) => {
447498
+ const signals2 = [];
447499
+ const binaryName = HOST_BINARIES[host.id];
447500
+ if (binaryName && isOnPath(binaryName, env4)) {
447501
+ signals2.push({ kind: "binary", name: binaryName });
447502
+ }
447503
+ const configDirRel = HOST_CONFIG_DIRS[host.id];
447504
+ if (configDirRel) {
447505
+ const path14 = join19(home, configDirRel);
447506
+ if (existsSync19(path14)) {
447507
+ signals2.push({ kind: "configDir", path: path14 });
447508
+ }
447509
+ }
447510
+ return {
447511
+ ...host,
447512
+ id: host.id,
447513
+ detected: signals2.length > 0,
447514
+ signals: signals2
447515
+ };
447516
+ });
447517
+ }
447518
+ function detectedAgentHosts(env4 = process.env) {
447519
+ return detectAgentHosts(env4).filter((h) => h.detected).sort((a, b) => {
447520
+ if (a.id === "claude-code") return -1;
447521
+ if (b.id === "claude-code") return 1;
447522
+ return a.id.localeCompare(b.id);
447523
+ });
447524
+ }
447525
+ function isOnPath(binary, env4) {
447526
+ const path14 = env4.PATH ?? env4.Path ?? "";
447527
+ const sep = process.platform === "win32" ? ";" : ":";
447528
+ for (const dir of path14.split(sep)) {
447529
+ if (!dir) continue;
447530
+ if (existsSync19(join19(dir, binary))) return true;
447531
+ if (process.platform === "win32" && existsSync19(join19(dir, `${binary}.exe`))) return true;
447532
+ }
447533
+ return false;
447534
+ }
447535
+ var HOST_BINARIES, HOST_CONFIG_DIRS;
447536
+ var init_agent_host_detect = __esm({
447537
+ "../cli/src/utils/agent-host-detect.ts"() {
447538
+ "use strict";
447539
+ HOST_BINARIES = {
447540
+ "claude-code": "claude",
447541
+ codex: "codex",
447542
+ cursor: "cursor",
447543
+ aider: "aider",
447544
+ "gemini-cli": "gemini",
447545
+ opencode: "opencode"
447546
+ };
447547
+ HOST_CONFIG_DIRS = {
447548
+ "claude-code": ".claude",
447549
+ codex: ".codex",
447550
+ cursor: ".cursor",
447551
+ // some installs; macOS app stores prefs elsewhere
447552
+ aider: null,
447553
+ "gemini-cli": ".gemini",
447554
+ // sst/opencode uses XDG-style `~/.config/opencode/` per
447555
+ // https://opencode.ai/docs/config/. The path is relative to $HOME so
447556
+ // the join in detectAgentHosts() resolves correctly.
447557
+ opencode: ".config/opencode"
447558
+ };
447559
+ }
447560
+ });
447561
+
447603
447562
  // ../cli/src/commands/_shared/hf-skill-bundle/bundle-content.ts
447604
447563
  var SKILL_MD, HOUSE_STYLE_MD, MOTION_PRINCIPLES_MD, TYPOGRAPHY_MD, TRANSITIONS_MD;
447605
447564
  var init_bundle_content = __esm({
@@ -448405,13 +448364,13 @@ Avoid transitions that create visible repeating geometric patterns \u2014 grids
448405
448364
  });
448406
448365
 
448407
448366
  // ../cli/src/commands/_shared/hf-skill-bundle/bundle.ts
448408
- import { readFileSync as readFileSync11, existsSync as existsSync21, statSync as statSync8 } from "node:fs";
448367
+ import { readFileSync as readFileSync11, existsSync as existsSync20, statSync as statSync8 } from "node:fs";
448409
448368
  import { join as join20 } from "node:path";
448410
448369
  import { homedir as homedir5 } from "node:os";
448411
448370
  import { createHash as createHash3 } from "node:crypto";
448412
448371
  function installedSkillPath() {
448413
448372
  const candidate = join20(homedir5(), ".claude", "skills", "hyperframes");
448414
- if (!existsSync21(candidate)) return null;
448373
+ if (!existsSync20(candidate)) return null;
448415
448374
  if (!statSync8(candidate).isDirectory()) return null;
448416
448375
  return candidate;
448417
448376
  }
@@ -448438,7 +448397,7 @@ function buildInstalled(skillRoot) {
448438
448397
  };
448439
448398
  const sections = [];
448440
448399
  for (const [label, path14] of Object.entries(installedPaths)) {
448441
- if (!existsSync21(path14)) return null;
448400
+ if (!existsSync20(path14)) return null;
448442
448401
  sections.push({ label, content: readFileSync11(path14, "utf-8") });
448443
448402
  }
448444
448403
  return {
@@ -448535,7 +448494,7 @@ function extractProjectFrontmatter(md) {
448535
448494
  if (!match2) return { frontmatter: void 0, remaining: md };
448536
448495
  let parsed;
448537
448496
  try {
448538
- parsed = (0, import_yaml4.parse)(match2[1]);
448497
+ parsed = (0, import_yaml3.parse)(match2[1]);
448539
448498
  } catch {
448540
448499
  return { frontmatter: void 0, remaining: md };
448541
448500
  }
@@ -448552,7 +448511,7 @@ function extractBeatCues(body) {
448552
448511
  if (!match2) return { cues: void 0, body };
448553
448512
  let parsed;
448554
448513
  try {
448555
- parsed = (0, import_yaml4.parse)(match2[1]);
448514
+ parsed = (0, import_yaml3.parse)(match2[1]);
448556
448515
  } catch {
448557
448516
  return { cues: void 0, body };
448558
448517
  }
@@ -448582,11 +448541,11 @@ function parseBeatDuration(body) {
448582
448541
  const n = parseFloat(valueMatch[1]);
448583
448542
  return Number.isFinite(n) && n > 0 ? n : void 0;
448584
448543
  }
448585
- var import_yaml4, HEADING_RE, BEAT_PREFIX_RE, DURATION_SUBSECTION_RE, DURATION_VALUE_RE, FRONTMATTER_RE, BEAT_CUES_RE;
448544
+ var import_yaml3, HEADING_RE, BEAT_PREFIX_RE, DURATION_SUBSECTION_RE, DURATION_VALUE_RE, FRONTMATTER_RE, BEAT_CUES_RE;
448586
448545
  var init_storyboard_parse = __esm({
448587
448546
  "../cli/src/commands/_shared/storyboard-parse.ts"() {
448588
448547
  "use strict";
448589
- import_yaml4 = __toESM(require_dist(), 1);
448548
+ import_yaml3 = __toESM(require_dist(), 1);
448590
448549
  HEADING_RE = /^##\s+(.+?)\s*$/gm;
448591
448550
  BEAT_PREFIX_RE = /^Beat\s+(.+?)\s+(?:—|:|-)\s+(.+)$/i;
448592
448551
  DURATION_SUBSECTION_RE = /###\s+Beat\s+duration\s*\n([\s\S]*?)(?=\n###\s|\n##\s|$)/i;
@@ -448670,9 +448629,9 @@ import OpenAI from "openai";
448670
448629
  import { GoogleGenerativeAI } from "@google/generative-ai";
448671
448630
  import { createHash as createHash4 } from "node:crypto";
448672
448631
  import { existsSync as existsSync23, mkdirSync as mkdirSync12 } from "node:fs";
448673
- import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile7 } from "node:fs/promises";
448632
+ import { mkdir as mkdir7, readFile as readFile5, writeFile as writeFile8 } from "node:fs/promises";
448674
448633
  import { homedir as homedir6 } from "node:os";
448675
- import { dirname as dirname14, join as join21, resolve as resolve17 } from "node:path";
448634
+ import { dirname as dirname13, join as join23, resolve as resolve15 } from "node:path";
448676
448635
  function buildSystemPrompt(ctx) {
448677
448636
  return `You are a Hyperframes composition author. The skill content below
448678
448637
  defines the framework rules, motion principles, and quality standards.
@@ -448820,7 +448779,7 @@ function computeCacheKey(parts) {
448820
448779
  return createHash4("sha256").update(parts.provider).update("\u241E").update(parts.model).update("\u241E").update(parts.systemPrompt).update("\u241E").update(parts.userPrompt).digest("hex");
448821
448780
  }
448822
448781
  function defaultCacheDir() {
448823
- return join21(homedir6(), ".vibeframe", "cache", "compose-scenes");
448782
+ return join23(homedir6(), ".vibeframe", "cache", "compose-scenes");
448824
448783
  }
448825
448784
  function extractHtml(responseText) {
448826
448785
  const fenced = responseText.match(/```html\s*\n([\s\S]*?)\n```/);
@@ -448844,9 +448803,9 @@ async function composeBeatHtml(ctx, overrides) {
448844
448803
  model: settings.model
448845
448804
  });
448846
448805
  const cacheDir = ctx.cacheDir ?? defaultCacheDir();
448847
- const cachePath = join21(cacheDir, `${cacheKey2}.html`);
448806
+ const cachePath = join23(cacheDir, `${cacheKey2}.html`);
448848
448807
  if (existsSync23(cachePath)) {
448849
- const html2 = await readFile7(cachePath, "utf-8");
448808
+ const html2 = await readFile5(cachePath, "utf-8");
448850
448809
  return { html: html2, cached: true, cacheKey: cacheKey2, model: settings.model, provider: ctx.provider };
448851
448810
  }
448852
448811
  if (!ctx.apiKey) {
@@ -448880,7 +448839,7 @@ async function composeBeatHtml(ctx, overrides) {
448880
448839
  const html = extractHtml(result.text);
448881
448840
  try {
448882
448841
  mkdirSync12(cacheDir, { recursive: true });
448883
- await writeFile7(cachePath, html, "utf-8");
448842
+ await writeFile8(cachePath, html, "utf-8");
448884
448843
  } catch {
448885
448844
  }
448886
448845
  const costUsd = result.inputTokens / 1e6 * settings.costPerMTokIn + result.outputTokens / 1e6 * settings.costPerMTokOut;
@@ -448938,17 +448897,17 @@ ${formatLintFeedback(lint2.findings)}`
448938
448897
  );
448939
448898
  }
448940
448899
  async function executeComposeScenesWithSkills(params, outputDir, overrides) {
448941
- const projectRoot = params.project ? resolve17(outputDir, params.project) : resolve17(outputDir);
448942
- const designPath = resolve17(projectRoot, params.design ?? "DESIGN.md");
448943
- const storyboardPath = resolve17(projectRoot, params.storyboard ?? "STORYBOARD.md");
448900
+ const projectRoot = params.project ? resolve15(outputDir, params.project) : resolve15(outputDir);
448901
+ const designPath = resolve15(projectRoot, params.design ?? "DESIGN.md");
448902
+ const storyboardPath = resolve15(projectRoot, params.storyboard ?? "STORYBOARD.md");
448944
448903
  if (!existsSync23(designPath)) {
448945
448904
  return { success: false, error: `DESIGN.md not found at ${designPath}` };
448946
448905
  }
448947
448906
  if (!existsSync23(storyboardPath)) {
448948
448907
  return { success: false, error: `STORYBOARD.md not found at ${storyboardPath}` };
448949
448908
  }
448950
- const designMd = await readFile7(designPath, "utf-8");
448951
- const storyboardMd = await readFile7(storyboardPath, "utf-8");
448909
+ const designMd = await readFile5(designPath, "utf-8");
448910
+ const storyboardMd = await readFile5(storyboardPath, "utf-8");
448952
448911
  const { global: storyboardGlobal, beats } = parseStoryboard(storyboardMd);
448953
448912
  if (beats.length === 0) {
448954
448913
  return {
@@ -448968,7 +448927,7 @@ async function executeComposeScenesWithSkills(params, outputDir, overrides) {
448968
448927
  provider = params.composer ?? "claude";
448969
448928
  apiKey = "";
448970
448929
  }
448971
- const compositionsDir = join21(projectRoot, "compositions");
448930
+ const compositionsDir = join23(projectRoot, "compositions");
448972
448931
  await mkdir7(compositionsDir, { recursive: true });
448973
448932
  const onProgress = params.onProgress ?? (() => {
448974
448933
  });
@@ -448989,9 +448948,9 @@ async function executeComposeScenesWithSkills(params, outputDir, overrides) {
448989
448948
  },
448990
448949
  overrides
448991
448950
  );
448992
- const compositionPath = join21(compositionsDir, `scene-${beat.id}.html`);
448993
- await mkdir7(dirname14(compositionPath), { recursive: true });
448994
- await writeFile7(compositionPath, result.html, "utf-8");
448951
+ const compositionPath = join23(compositionsDir, `scene-${beat.id}.html`);
448952
+ await mkdir7(dirname13(compositionPath), { recursive: true });
448953
+ await writeFile8(compositionPath, result.html, "utf-8");
448995
448954
  if (result.cached) {
448996
448955
  onProgress({
448997
448956
  type: "beat-cached",
@@ -449170,14 +449129,14 @@ var init_compose_scenes_skills = __esm({
449170
449129
 
449171
449130
  // ../cli/src/commands/_shared/compose-prompts.ts
449172
449131
  import { existsSync as existsSync24 } from "node:fs";
449173
- import { readFile as readFile8 } from "node:fs/promises";
449174
- import { join as join23, relative as relative5, resolve as resolve18 } from "node:path";
449132
+ import { readFile as readFile6 } from "node:fs/promises";
449133
+ import { join as join24, relative as relative5, resolve as resolve16 } from "node:path";
449175
449134
  async function getComposePrompts(opts) {
449176
- const projectDir = resolve18(opts.projectDir);
449177
- const designPath = join23(projectDir, "DESIGN.md");
449178
- const storyboardPath = join23(projectDir, "STORYBOARD.md");
449179
- const skillPath = join23(projectDir, "SKILL.md");
449180
- const compositionsDir = join23(projectDir, "compositions");
449135
+ const projectDir = resolve16(opts.projectDir);
449136
+ const designPath = join24(projectDir, "DESIGN.md");
449137
+ const storyboardPath = join24(projectDir, "STORYBOARD.md");
449138
+ const skillPath = join24(projectDir, "SKILL.md");
449139
+ const compositionsDir = join24(projectDir, "compositions");
449181
449140
  const warnings = [];
449182
449141
  const baseError = (msg) => ({
449183
449142
  success: false,
@@ -449203,7 +449162,7 @@ async function getComposePrompts(opts) {
449203
449162
  "SKILL.md not installed \u2014 host agent won't have Hyperframes rules in context. Run `vibe scene install-skill` to install it."
449204
449163
  );
449205
449164
  }
449206
- const storyboardMd = await readFile8(storyboardPath, "utf-8");
449165
+ const storyboardMd = await readFile6(storyboardPath, "utf-8");
449207
449166
  const parsed = parseStoryboard(storyboardMd);
449208
449167
  if (parsed.beats.length === 0) {
449209
449168
  return baseError(`STORYBOARD.md has no \`## Beat \u2026\` headings.`);
@@ -449221,7 +449180,7 @@ async function getComposePrompts(opts) {
449221
449180
  beats = parsed.beats;
449222
449181
  }
449223
449182
  const result = beats.map((beat) => {
449224
- const outputPathAbs = join23(compositionsDir, `scene-${beat.id}.html`);
449183
+ const outputPathAbs = join24(compositionsDir, `scene-${beat.id}.html`);
449225
449184
  const outputPathRel = relative5(projectDir, outputPathAbs);
449226
449185
  const userPrompt = buildUserPrompt({
449227
449186
  beat,
@@ -449285,124 +449244,425 @@ var init_compose_prompts = __esm({
449285
449244
  }
449286
449245
  });
449287
449246
 
449288
- // ../cli/src/utils/agent-host-detect.ts
449289
- import { existsSync as existsSync25 } from "node:fs";
449247
+ // ../cli/src/pipeline/renderers/chrome.ts
449248
+ import { existsSync as existsSync26 } from "node:fs";
449290
449249
  import { homedir as homedir7 } from "node:os";
449291
- import { join as join24 } from "node:path";
449292
- function detectAgentHosts(env4 = process.env) {
449293
- const home = homedir7();
449294
- return [
449295
- {
449296
- id: "claude-code",
449297
- label: "Claude Code",
449298
- detected: false,
449299
- signals: [],
449300
- projectFiles: ["CLAUDE.md", ".claude/skills/"]
449301
- },
449302
- {
449303
- id: "codex",
449304
- label: "Codex (OpenAI)",
449305
- detected: false,
449306
- signals: [],
449307
- projectFiles: ["AGENTS.md"]
449308
- },
449309
- {
449310
- id: "cursor",
449311
- label: "Cursor",
449312
- detected: false,
449313
- signals: [],
449314
- projectFiles: ["AGENTS.md", ".cursor/rules/"]
449315
- },
449316
- {
449317
- id: "aider",
449318
- label: "Aider",
449319
- detected: false,
449320
- signals: [],
449321
- projectFiles: ["AGENTS.md", ".aider.conf.yml"]
449322
- },
449323
- {
449324
- id: "gemini-cli",
449325
- label: "Gemini CLI",
449326
- detected: false,
449327
- signals: [],
449328
- // Per https://geminicli.com/docs/cli/gemini-md/ Gemini CLI's primary
449329
- // context file is GEMINI.md; AGENTS.md is the cross-tool fallback
449330
- // VibeFrame writes by default. Both are honoured.
449331
- projectFiles: ["GEMINI.md", "AGENTS.md", ".gemini/"]
449332
- },
449333
- {
449334
- id: "opencode",
449335
- label: "OpenCode",
449336
- detected: false,
449337
- signals: [],
449338
- // sst/opencode officially supports the agents.md spec — AGENTS.md at
449339
- // project root is the standard place. Local config also under
449340
- // `.opencode/` per https://opencode.ai/docs/config/.
449341
- projectFiles: ["AGENTS.md", ".opencode/"]
449250
+ import { join as join25 } from "node:path";
449251
+ function findChrome() {
449252
+ const candidates = [
449253
+ process.env.HYPERFRAMES_CHROME_PATH,
449254
+ process.env.CHROME_PATH,
449255
+ // puppeteer auto-downloaded headless shell
449256
+ join25(
449257
+ homedir7(),
449258
+ ".cache",
449259
+ "puppeteer",
449260
+ "chrome-headless-shell",
449261
+ "mac_arm-147.0.7727.56",
449262
+ "chrome-headless-shell-mac_arm",
449263
+ "chrome-headless-shell"
449264
+ ),
449265
+ // system Chrome / Chromium
449266
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
449267
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
449268
+ "/usr/bin/google-chrome",
449269
+ "/usr/bin/chromium",
449270
+ "/usr/bin/chromium-browser",
449271
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
449272
+ ];
449273
+ for (const c of candidates) {
449274
+ if (c && existsSync26(c)) return c;
449275
+ }
449276
+ return void 0;
449277
+ }
449278
+ async function preflightChrome() {
449279
+ return findChrome() ? { ok: true } : { ok: false, reason: CHROME_NOT_FOUND_REASON };
449280
+ }
449281
+ var CHROME_NOT_FOUND_REASON;
449282
+ var init_chrome3 = __esm({
449283
+ "../cli/src/pipeline/renderers/chrome.ts"() {
449284
+ "use strict";
449285
+ CHROME_NOT_FOUND_REASON = "Chrome not found. Set HYPERFRAMES_CHROME_PATH, or install Chrome (macOS: brew install --cask google-chrome \xB7 Linux: apt install chromium). Run `vibe doctor` for details.";
449286
+ }
449287
+ });
449288
+
449289
+ // ../cli/src/commands/_shared/scene-audio-scan.ts
449290
+ import { readFile as readFile8 } from "node:fs/promises";
449291
+ import { resolve as resolve18 } from "node:path";
449292
+ function parseRootClips(rootHtml) {
449293
+ const clips = [];
449294
+ const clipRegex = /<div\b[^>]*class="clip"[^>]*>/gi;
449295
+ let match2;
449296
+ while ((match2 = clipRegex.exec(rootHtml)) !== null) {
449297
+ const tag = match2[0];
449298
+ const compositionId = pickAttr(tag, "data-composition-id");
449299
+ const compositionSrc = pickAttr(tag, "data-composition-src");
449300
+ const start = pickNumberAttr(tag, "data-start");
449301
+ const duration = pickNumberAttr(tag, "data-duration");
449302
+ const trackIndex = pickNumberAttr(tag, "data-track-index") ?? 1;
449303
+ if (!compositionId || !compositionSrc || start === null || duration === null) {
449304
+ continue;
449342
449305
  }
449343
- ].map((host) => {
449344
- const signals2 = [];
449345
- const binaryName = HOST_BINARIES[host.id];
449346
- if (binaryName && isOnPath(binaryName, env4)) {
449347
- signals2.push({ kind: "binary", name: binaryName });
449306
+ clips.push({ compositionId, compositionSrc, start, duration, trackIndex });
449307
+ }
449308
+ return clips;
449309
+ }
449310
+ function parseSceneAudios(compositionHtml) {
449311
+ const out = [];
449312
+ const audioRegex = /<audio\b([^>]*)>/gi;
449313
+ let match2;
449314
+ while ((match2 = audioRegex.exec(compositionHtml)) !== null) {
449315
+ const attrs = match2[1];
449316
+ const src = pickAttr(attrs, "src");
449317
+ if (!src) continue;
449318
+ const localStart = pickNumberAttr(attrs, "data-start") ?? 0;
449319
+ const durationRaw = pickAttr(attrs, "data-duration");
449320
+ const durationHint = !durationRaw || durationRaw === "auto" ? "auto" : Number(durationRaw);
449321
+ const volume = pickNumberAttr(attrs, "data-volume") ?? 1;
449322
+ const trackIndex = pickNumberAttr(attrs, "data-track-index") ?? 2;
449323
+ out.push({ srcRel: src, localStart, durationHint, volume, trackIndex });
449324
+ }
449325
+ return out;
449326
+ }
449327
+ function makeFsCompositionReader(projectDir) {
449328
+ return async (compositionSrcRel) => {
449329
+ const abs = resolve18(projectDir, compositionSrcRel);
449330
+ try {
449331
+ return await readFile8(abs, "utf-8");
449332
+ } catch {
449333
+ return null;
449348
449334
  }
449349
- const configDirRel = HOST_CONFIG_DIRS[host.id];
449350
- if (configDirRel) {
449351
- const path14 = join24(home, configDirRel);
449352
- if (existsSync25(path14)) {
449353
- signals2.push({ kind: "configDir", path: path14 });
449354
- }
449335
+ };
449336
+ }
449337
+ async function scanSceneAudio(opts) {
449338
+ const reader = opts.readComposition ?? makeFsCompositionReader(opts.projectDir);
449339
+ const clips = parseRootClips(opts.rootHtml);
449340
+ const out = [];
449341
+ for (const clip of clips) {
449342
+ const html = await reader(clip.compositionSrc);
449343
+ if (!html) continue;
449344
+ const audios = parseSceneAudios(html);
449345
+ for (const audio of audios) {
449346
+ out.push({
449347
+ srcRel: audio.srcRel,
449348
+ srcAbs: resolve18(opts.projectDir, audio.srcRel),
449349
+ absoluteStart: clip.start + audio.localStart,
449350
+ durationHint: audio.durationHint,
449351
+ clipDurationCap: clip.duration - audio.localStart,
449352
+ volume: audio.volume,
449353
+ trackIndex: audio.trackIndex,
449354
+ compositionSrc: clip.compositionSrc
449355
+ });
449355
449356
  }
449357
+ }
449358
+ const maxClipEnd = clips.reduce((m, c) => Math.max(m, c.start + c.duration), 0);
449359
+ const rootAudios = parseSceneAudios(opts.rootHtml);
449360
+ for (const audio of rootAudios) {
449361
+ out.push({
449362
+ srcRel: audio.srcRel,
449363
+ srcAbs: resolve18(opts.projectDir, audio.srcRel),
449364
+ absoluteStart: audio.localStart,
449365
+ durationHint: audio.durationHint,
449366
+ clipDurationCap: Math.max(0, maxClipEnd - audio.localStart),
449367
+ volume: audio.volume,
449368
+ trackIndex: audio.trackIndex,
449369
+ compositionSrc: "(root)"
449370
+ });
449371
+ }
449372
+ out.sort((a, b) => a.absoluteStart - b.absoluteStart);
449373
+ return out;
449374
+ }
449375
+ function pickAttr(tag, name) {
449376
+ const re = new RegExp(`\\b${escapeRegex3(name)}\\s*=\\s*("([^"]*)"|'([^']*)')`);
449377
+ const m = tag.match(re);
449378
+ if (!m) return null;
449379
+ return m[2] ?? m[3] ?? null;
449380
+ }
449381
+ function pickNumberAttr(tag, name) {
449382
+ const raw2 = pickAttr(tag, name);
449383
+ if (raw2 === null) return null;
449384
+ const n = Number(raw2);
449385
+ return Number.isFinite(n) ? n : null;
449386
+ }
449387
+ function escapeRegex3(s) {
449388
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
449389
+ }
449390
+ var init_scene_audio_scan = __esm({
449391
+ "../cli/src/commands/_shared/scene-audio-scan.ts"() {
449392
+ "use strict";
449393
+ }
449394
+ });
449395
+
449396
+ // ../cli/src/commands/_shared/scene-audio-mux.ts
449397
+ import { rename as rename3, unlink as unlink4 } from "node:fs/promises";
449398
+ import { resolve as resolve19, dirname as dirname15, extname as extname4, basename as basename4 } from "node:path";
449399
+ function buildAudioMuxFilter(audios) {
449400
+ if (audios.length === 0) return null;
449401
+ const labels = [];
449402
+ const stages = [];
449403
+ audios.forEach((a, i) => {
449404
+ const inputIdx = i + 1;
449405
+ const delayMs = Math.max(0, Math.round(a.absoluteStart * 1e3));
449406
+ const volume = Number.isFinite(a.volume) ? a.volume : 1;
449407
+ const durationHint = typeof a.durationHint === "number" && Number.isFinite(a.durationHint) ? Math.max(0, a.durationHint) : null;
449408
+ const clipCap = Math.max(0, a.clipDurationCap);
449409
+ const trimSec = durationHint === null ? clipCap : Math.min(durationHint, clipCap);
449410
+ const label = `a${i}`;
449411
+ const stage = [
449412
+ `[${inputIdx}:a]`,
449413
+ `atrim=duration=${trimSec.toFixed(3)},`,
449414
+ `asetpts=PTS-STARTPTS,`,
449415
+ `adelay=${delayMs}:all=1,`,
449416
+ `volume=${volume}`,
449417
+ `[${label}]`
449418
+ ].join("");
449419
+ stages.push(stage);
449420
+ labels.push(`[${label}]`);
449421
+ });
449422
+ if (audios.length === 1) {
449356
449423
  return {
449357
- ...host,
449358
- id: host.id,
449359
- detected: signals2.length > 0,
449360
- signals: signals2
449424
+ filterComplex: stages.join(";"),
449425
+ outLabel: labels[0],
449426
+ inputCount: 1
449361
449427
  };
449362
- });
449428
+ }
449429
+ const mix = `${labels.join("")}amix=inputs=${audios.length}:dropout_transition=0:normalize=0[mixed]`;
449430
+ return {
449431
+ filterComplex: `${stages.join(";")};${mix}`,
449432
+ outLabel: "[mixed]",
449433
+ inputCount: audios.length
449434
+ };
449363
449435
  }
449364
- function detectedAgentHosts(env4 = process.env) {
449365
- return detectAgentHosts(env4).filter((h) => h.detected).sort((a, b) => {
449366
- if (a.id === "claude-code") return -1;
449367
- if (b.id === "claude-code") return 1;
449368
- return a.id.localeCompare(b.id);
449369
- });
449436
+ function audioCodecForFormat(format4) {
449437
+ if (format4 === "webm") return "libopus";
449438
+ if (format4 === "mov") return "pcm_s16le";
449439
+ return "aac";
449370
449440
  }
449371
- function isOnPath(binary, env4) {
449372
- const path14 = env4.PATH ?? env4.Path ?? "";
449373
- const sep = process.platform === "win32" ? ";" : ":";
449374
- for (const dir of path14.split(sep)) {
449375
- if (!dir) continue;
449376
- if (existsSync25(join24(dir, binary))) return true;
449377
- if (process.platform === "win32" && existsSync25(join24(dir, `${binary}.exe`))) return true;
449441
+ async function muxAudioIntoVideo(opts) {
449442
+ if (opts.audios.length === 0) {
449443
+ return { success: true, outputPath: opts.videoPath, audioCount: 0 };
449378
449444
  }
449379
- return false;
449445
+ if (!commandExists("ffmpeg")) {
449446
+ return {
449447
+ success: false,
449448
+ outputPath: opts.videoPath,
449449
+ audioCount: opts.audios.length,
449450
+ error: "ffmpeg not found in PATH \u2014 install via `brew install ffmpeg` (mac) or your package manager"
449451
+ };
449452
+ }
449453
+ const filter4 = buildAudioMuxFilter(opts.audios);
449454
+ if (!filter4) {
449455
+ return { success: true, outputPath: opts.videoPath, audioCount: 0 };
449456
+ }
449457
+ const ext = extname4(opts.videoPath) || `.${opts.format}`;
449458
+ const tmpPath = resolve19(
449459
+ dirname15(opts.videoPath),
449460
+ `.${basename4(opts.videoPath, ext)}.muxing${ext}`
449461
+ );
449462
+ const args = ["-y", "-loglevel", "error", "-i", opts.videoPath];
449463
+ for (const a of opts.audios) {
449464
+ args.push("-i", a.srcAbs);
449465
+ }
449466
+ args.push(
449467
+ "-filter_complex",
449468
+ filter4.filterComplex,
449469
+ "-map",
449470
+ "0:v",
449471
+ "-map",
449472
+ filter4.outLabel,
449473
+ "-c:v",
449474
+ "copy",
449475
+ "-c:a",
449476
+ audioCodecForFormat(opts.format),
449477
+ // Cap on the video duration so audio that overruns the producer's render
449478
+ // (e.g. a long Kokoro wav on a short scene) doesn't extend the output.
449479
+ // Video drives the timeline because the producer already counted frames.
449480
+ "-t",
449481
+ opts.totalDuration && opts.totalDuration > 0 ? opts.totalDuration.toFixed(3) : opts.videoDuration?.toFixed(3) ?? ""
449482
+ );
449483
+ if (args[args.length - 1] === "") {
449484
+ args.pop();
449485
+ args.pop();
449486
+ }
449487
+ args.push("-movflags", "+faststart", tmpPath);
449488
+ try {
449489
+ const { stderr } = await execSafe("ffmpeg", args);
449490
+ if (stderr && opts.onProgress) {
449491
+ stderr.split(/\r?\n/).forEach((line) => opts.onProgress?.(line));
449492
+ }
449493
+ } catch (err) {
449494
+ const msg = err instanceof Error ? err.message : String(err);
449495
+ try {
449496
+ await unlink4(tmpPath);
449497
+ } catch {
449498
+ }
449499
+ return {
449500
+ success: false,
449501
+ outputPath: opts.videoPath,
449502
+ audioCount: opts.audios.length,
449503
+ error: `ffmpeg mux failed: ${msg}`
449504
+ };
449505
+ }
449506
+ await rename3(tmpPath, opts.videoPath);
449507
+ return {
449508
+ success: true,
449509
+ outputPath: opts.videoPath,
449510
+ audioCount: opts.audios.length
449511
+ };
449380
449512
  }
449381
- var HOST_BINARIES, HOST_CONFIG_DIRS;
449382
- var init_agent_host_detect = __esm({
449383
- "../cli/src/utils/agent-host-detect.ts"() {
449513
+ var init_scene_audio_mux = __esm({
449514
+ "../cli/src/commands/_shared/scene-audio-mux.ts"() {
449384
449515
  "use strict";
449385
- HOST_BINARIES = {
449386
- "claude-code": "claude",
449387
- codex: "codex",
449388
- cursor: "cursor",
449389
- aider: "aider",
449390
- "gemini-cli": "gemini",
449391
- opencode: "opencode"
449516
+ init_exec_safe();
449517
+ }
449518
+ });
449519
+
449520
+ // ../cli/src/commands/_shared/scene-render.ts
449521
+ var scene_render_exports = {};
449522
+ __export(scene_render_exports, {
449523
+ buildRenderConfig: () => buildRenderConfig,
449524
+ defaultOutputPath: () => defaultOutputPath,
449525
+ executeSceneRender: () => executeSceneRender,
449526
+ qualityToCrf: () => qualityToCrf
449527
+ });
449528
+ import { mkdir as mkdir9, readFile as readFile9, stat as stat2 } from "node:fs/promises";
449529
+ import { existsSync as existsSync27 } from "node:fs";
449530
+ import { resolve as resolve20, relative as relative7, dirname as dirname16, basename as basename5 } from "node:path";
449531
+ function qualityToCrf(quality = "standard") {
449532
+ return quality === "draft" ? 28 : quality === "high" ? 18 : 23;
449533
+ }
449534
+ function defaultOutputPath(opts) {
449535
+ const fmt = opts.format ?? "mp4";
449536
+ const now = opts.now ?? /* @__PURE__ */ new Date();
449537
+ const stamp = now.toISOString().replace(/[:T]/g, "-").replace(/\..+$/, "");
449538
+ const name = (opts.projectName ?? basename5(resolve20(opts.projectDir))) || "scene";
449539
+ return resolve20(opts.projectDir, "renders", `${name}-${stamp}.${fmt}`);
449540
+ }
449541
+ async function readProjectName(projectDir) {
449542
+ const cfgPath = resolve20(projectDir, "vibe.project.yaml");
449543
+ if (!existsSync27(cfgPath)) return void 0;
449544
+ try {
449545
+ const raw2 = await (await import("node:fs/promises")).readFile(cfgPath, "utf-8");
449546
+ const parsed = (0, import_yaml5.parse)(raw2);
449547
+ return parsed?.name ?? void 0;
449548
+ } catch {
449549
+ return void 0;
449550
+ }
449551
+ }
449552
+ function buildRenderConfig(opts) {
449553
+ const quality = opts.quality ?? "standard";
449554
+ return {
449555
+ fps: opts.fps ?? 30,
449556
+ quality,
449557
+ format: opts.format ?? "mp4",
449558
+ entryFile: opts.entryFile ?? "index.html",
449559
+ crf: qualityToCrf(quality),
449560
+ workers: opts.workers ?? 1
449561
+ };
449562
+ }
449563
+ async function executeSceneRender(opts = {}) {
449564
+ const projectDir = resolve20(opts.projectDir ?? ".");
449565
+ const root2 = opts.root ?? "index.html";
449566
+ const projectStat = await safeStat(projectDir);
449567
+ if (!projectStat || !projectStat.isDirectory()) {
449568
+ return { success: false, error: `Project directory not found: ${projectDir}` };
449569
+ }
449570
+ if (!await rootExists(projectDir, root2)) {
449571
+ return {
449572
+ success: false,
449573
+ error: `Root composition not found: ${resolve20(projectDir, root2)}. Run \`vibe scene init\` first.`
449392
449574
  };
449393
- HOST_CONFIG_DIRS = {
449394
- "claude-code": ".claude",
449395
- codex: ".codex",
449396
- cursor: ".cursor",
449397
- // some installs; macOS app stores prefs elsewhere
449398
- aider: null,
449399
- "gemini-cli": ".gemini",
449400
- // sst/opencode uses XDG-style `~/.config/opencode/` per
449401
- // https://opencode.ai/docs/config/. The path is relative to $HOME so
449402
- // the join in detectAgentHosts() resolves correctly.
449403
- opencode: ".config/opencode"
449575
+ }
449576
+ const chrome2 = await preflightChrome();
449577
+ if (!chrome2.ok) {
449578
+ return { success: false, error: chrome2.reason };
449579
+ }
449580
+ const projectName = await readProjectName(projectDir);
449581
+ const outputPath = opts.output ? resolve20(projectDir, opts.output) : defaultOutputPath({ projectDir, projectName, format: opts.format });
449582
+ await mkdir9(dirname16(outputPath), { recursive: true });
449583
+ const config4 = buildRenderConfig({
449584
+ fps: opts.fps,
449585
+ quality: opts.quality,
449586
+ format: opts.format,
449587
+ workers: opts.workers,
449588
+ entryFile: root2
449589
+ });
449590
+ const job = createRenderJob(config4);
449591
+ const start = Date.now();
449592
+ try {
449593
+ await executeRenderJob(
449594
+ job,
449595
+ projectDir,
449596
+ outputPath,
449597
+ (j, msg) => opts.onProgress?.(j.progress, j.currentStage ?? msg),
449598
+ opts.signal
449599
+ );
449600
+ } catch (err) {
449601
+ return {
449602
+ success: false,
449603
+ error: err instanceof Error ? err.message : String(err)
449404
449604
  };
449405
449605
  }
449606
+ let audioCount = 0;
449607
+ let audioMuxApplied = false;
449608
+ let audioMuxWarning;
449609
+ try {
449610
+ opts.onProgress?.(0.95, "Mixing audio");
449611
+ const rootHtml = await readFile9(resolve20(projectDir, root2), "utf-8");
449612
+ const audios = await scanSceneAudio({ projectDir, rootHtml });
449613
+ audioCount = audios.length;
449614
+ if (audios.length > 0) {
449615
+ const videoDuration = job.totalFrames && config4.fps ? job.totalFrames / config4.fps : void 0;
449616
+ const mux = await muxAudioIntoVideo({
449617
+ videoPath: outputPath,
449618
+ audios,
449619
+ format: config4.format ?? "mp4",
449620
+ videoDuration,
449621
+ onProgress: (line) => {
449622
+ if (line) opts.onProgress?.(0.97, line);
449623
+ }
449624
+ });
449625
+ if (mux.success) {
449626
+ audioMuxApplied = true;
449627
+ } else {
449628
+ audioMuxWarning = mux.error;
449629
+ }
449630
+ }
449631
+ } catch (err) {
449632
+ audioMuxWarning = err instanceof Error ? err.message : String(err);
449633
+ }
449634
+ return {
449635
+ success: true,
449636
+ outputPath: relative7(process.cwd(), outputPath) || outputPath,
449637
+ durationMs: Date.now() - start,
449638
+ framesRendered: job.framesRendered,
449639
+ totalFrames: job.totalFrames,
449640
+ fps: config4.fps,
449641
+ quality: config4.quality,
449642
+ format: config4.format,
449643
+ audioCount,
449644
+ audioMuxApplied,
449645
+ audioMuxWarning
449646
+ };
449647
+ }
449648
+ async function safeStat(p) {
449649
+ try {
449650
+ return await stat2(p);
449651
+ } catch {
449652
+ return null;
449653
+ }
449654
+ }
449655
+ var import_yaml5;
449656
+ var init_scene_render = __esm({
449657
+ "../cli/src/commands/_shared/scene-render.ts"() {
449658
+ "use strict";
449659
+ import_yaml5 = __toESM(require_dist(), 1);
449660
+ init_dist2();
449661
+ init_chrome3();
449662
+ init_scene_lint();
449663
+ init_scene_audio_scan();
449664
+ init_scene_audio_mux();
449665
+ }
449406
449666
  });
449407
449667
 
449408
449668
  // ../cli/src/commands/_shared/scene-build.ts
@@ -449411,23 +449671,23 @@ __export(scene_build_exports, {
449411
449671
  executeSceneBuild: () => executeSceneBuild,
449412
449672
  resolveSceneBuildMode: () => resolveSceneBuildMode
449413
449673
  });
449414
- import { existsSync as existsSync26 } from "node:fs";
449415
- import { mkdir as mkdir8, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
449416
- import { dirname as dirname15, join as join25, resolve as resolve19 } from "node:path";
449674
+ import { existsSync as existsSync28 } from "node:fs";
449675
+ import { mkdir as mkdir10, readFile as readFile10, writeFile as writeFile10 } from "node:fs/promises";
449676
+ import { dirname as dirname17, join as join26, resolve as resolve21 } from "node:path";
449417
449677
  async function executeSceneBuild(opts) {
449418
449678
  const startedAt = Date.now();
449419
- const projectDir = resolve19(opts.projectDir);
449679
+ const projectDir = resolve21(opts.projectDir);
449420
449680
  const onProgress = opts.onProgress ?? (() => {
449421
449681
  });
449422
449682
  const mode = resolveSceneBuildMode(opts);
449423
- const storyboardPath = join25(projectDir, "STORYBOARD.md");
449424
- if (!existsSync26(storyboardPath)) {
449683
+ const storyboardPath = join26(projectDir, "STORYBOARD.md");
449684
+ if (!existsSync28(storyboardPath)) {
449425
449685
  return failBeforePrimitives(
449426
449686
  `STORYBOARD.md not found at ${storyboardPath}. Run \`vibe scene init <dir>\` to create a starter, or add STORYBOARD.md with per-beat cues.`,
449427
449687
  startedAt
449428
449688
  );
449429
449689
  }
449430
- const storyboardMd = await readFile9(storyboardPath, "utf-8");
449690
+ const storyboardMd = await readFile10(storyboardPath, "utf-8");
449431
449691
  const parsed = parseStoryboard(storyboardMd);
449432
449692
  if (parsed.beats.length === 0) {
449433
449693
  return failBeforePrimitives(
@@ -449455,9 +449715,9 @@ async function executeSceneBuild(opts) {
449455
449715
  );
449456
449716
  let composeData;
449457
449717
  if (mode === "agent") {
449458
- const compositionsDir = join25(projectDir, "compositions");
449718
+ const compositionsDir = join26(projectDir, "compositions");
449459
449719
  const missingBeats = parsed.beats.filter(
449460
- (b) => !existsSync26(join25(compositionsDir, `scene-${b.id}.html`))
449720
+ (b) => !existsSync28(join26(compositionsDir, `scene-${b.id}.html`))
449461
449721
  );
449462
449722
  if (missingBeats.length > 0) {
449463
449723
  const plan = await getComposePrompts({ projectDir });
@@ -449505,7 +449765,7 @@ async function executeSceneBuild(opts) {
449505
449765
  }
449506
449766
  composeData = composeResult.data;
449507
449767
  }
449508
- if (!existsSync26(join25(projectDir, "index.html"))) {
449768
+ if (!existsSync28(join26(projectDir, "index.html"))) {
449509
449769
  await scaffoldSceneProject({
449510
449770
  dir: projectDir,
449511
449771
  name: projectDir.split(/[\\/]/).filter(Boolean).pop(),
@@ -449565,7 +449825,7 @@ async function dispatchNarration(beat, ctx) {
449565
449825
  if (!text) return { status: "no-cue" };
449566
449826
  for (const ext of ["mp3", "wav"]) {
449567
449827
  const rel2 = `assets/narration-${beat.id}.${ext}`;
449568
- if (existsSync26(join25(ctx.projectDir, rel2)) && !ctx.force) {
449828
+ if (existsSync28(join26(ctx.projectDir, rel2)) && !ctx.force) {
449569
449829
  ctx.onProgress({ type: "narration-cached", beatId: beat.id, path: rel2 });
449570
449830
  return { status: "cached", path: rel2 };
449571
449831
  }
@@ -449585,9 +449845,9 @@ async function dispatchNarration(beat, ctx) {
449585
449845
  return { status: "failed", error };
449586
449846
  }
449587
449847
  const rel = `assets/narration-${beat.id}.${resolution.audioExtension}`;
449588
- const abs = join25(ctx.projectDir, rel);
449589
- await mkdir8(dirname15(abs), { recursive: true });
449590
- await writeFile8(abs, result.audioBuffer);
449848
+ const abs = join26(ctx.projectDir, rel);
449849
+ await mkdir10(dirname17(abs), { recursive: true });
449850
+ await writeFile10(abs, result.audioBuffer);
449591
449851
  ctx.onProgress({
449592
449852
  type: "narration-generated",
449593
449853
  beatId: beat.id,
@@ -449605,8 +449865,8 @@ async function dispatchBackdrop(beat, ctx) {
449605
449865
  return { status: "failed", error };
449606
449866
  }
449607
449867
  const rel = `assets/backdrop-${beat.id}.png`;
449608
- const abs = join25(ctx.projectDir, rel);
449609
- if (existsSync26(abs) && !ctx.force) {
449868
+ const abs = join26(ctx.projectDir, rel);
449869
+ if (existsSync28(abs) && !ctx.force) {
449610
449870
  ctx.onProgress({ type: "backdrop-cached", beatId: beat.id, path: rel });
449611
449871
  return { status: "cached", path: rel };
449612
449872
  }
@@ -449629,8 +449889,8 @@ async function dispatchBackdrop(beat, ctx) {
449629
449889
  ctx.onProgress({ type: "backdrop-failed", beatId: beat.id, error });
449630
449890
  return { status: "failed", error };
449631
449891
  }
449632
- await mkdir8(dirname15(abs), { recursive: true });
449633
- await writeFile8(abs, Buffer.from(result.images[0].base64, "base64"));
449892
+ await mkdir10(dirname17(abs), { recursive: true });
449893
+ await writeFile10(abs, Buffer.from(result.images[0].base64, "base64"));
449634
449894
  ctx.onProgress({
449635
449895
  type: "backdrop-generated",
449636
449896
  beatId: beat.id,
@@ -449640,15 +449900,15 @@ async function dispatchBackdrop(beat, ctx) {
449640
449900
  return { status: "generated", path: rel };
449641
449901
  }
449642
449902
  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 });
449903
+ (0, import_dotenv2.config)({ path: join26(projectDir, ".env"), quiet: true });
449904
+ (0, import_dotenv2.config)({ path: resolve21(process.cwd(), ".env"), quiet: true });
449645
449905
  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 });
449906
+ while (dir !== dirname17(dir)) {
449907
+ if (existsSync28(join26(dir, "pnpm-workspace.yaml"))) {
449908
+ (0, import_dotenv2.config)({ path: join26(dir, ".env"), quiet: true });
449649
449909
  return;
449650
449910
  }
449651
- dir = dirname15(dir);
449911
+ dir = dirname17(dir);
449652
449912
  }
449653
449913
  }
449654
449914
  async function skipped(kind, beatId, reason, ctx) {
@@ -449674,9 +449934,9 @@ function resolveSceneBuildMode(opts) {
449674
449934
  return detectedAgentHosts().length > 0 ? "agent" : "batch";
449675
449935
  }
449676
449936
  async function syncRootClipReferences(beats, projectDir, outcomes) {
449677
- const rootPath = join25(projectDir, "index.html");
449678
- if (!existsSync26(rootPath)) return;
449679
- const html = await readFile9(rootPath, "utf-8");
449937
+ const rootPath = join26(projectDir, "index.html");
449938
+ if (!existsSync28(rootPath)) return;
449939
+ const html = await readFile10(rootPath, "utf-8");
449680
449940
  let cursor = 0;
449681
449941
  const clipLines = [];
449682
449942
  const audioLines = [];
@@ -449720,14 +449980,14 @@ ${block}
449720
449980
  `$1${totalDuration}$3`
449721
449981
  );
449722
449982
  if (next !== html) {
449723
- await writeFile8(rootPath, next, "utf-8");
449983
+ await writeFile10(rootPath, next, "utf-8");
449724
449984
  }
449725
449985
  }
449726
449986
  async function resolveBeatDuration(opts) {
449727
449987
  const storyboardMin = opts.beatDuration ?? 3;
449728
449988
  if (!opts.narrationPath) return Number(storyboardMin.toFixed(2));
449729
449989
  try {
449730
- const audioDuration = await getAudioDuration(join25(opts.projectDir, opts.narrationPath));
449990
+ const audioDuration = await getAudioDuration(join26(opts.projectDir, opts.narrationPath));
449731
449991
  return Number(Math.max(storyboardMin, audioDuration + 0.5).toFixed(2));
449732
449992
  } catch {
449733
449993
  return Number(storyboardMin.toFixed(2));
@@ -449750,269 +450010,6 @@ var init_scene_build = __esm({
449750
450010
  }
449751
450011
  });
449752
450012
 
449753
- // ../cli/src/commands/output.ts
449754
- var output_exports = {};
449755
- __export(output_exports, {
449756
- COST_ESTIMATES: () => COST_ESTIMATES,
449757
- ExitCode: () => ExitCode,
449758
- _resetDeprecationMemoryForTesting: () => _resetDeprecationMemoryForTesting,
449759
- apiError: () => apiError,
449760
- authError: () => authError,
449761
- emitDeprecationWarning: () => emitDeprecationWarning,
449762
- exitWithError: () => exitWithError,
449763
- generalError: () => generalError,
449764
- isJsonMode: () => isJsonMode,
449765
- isQuietMode: () => isQuietMode,
449766
- log: () => log,
449767
- networkError: () => networkError,
449768
- notFoundError: () => notFoundError,
449769
- outputError: () => outputError,
449770
- outputResult: () => outputResult,
449771
- outputSuccess: () => outputSuccess,
449772
- spinner: () => spinner,
449773
- suggestNext: () => suggestNext,
449774
- usageError: () => usageError
449775
- });
449776
- function usageError(msg, suggestion) {
449777
- return { success: false, error: msg, code: "USAGE_ERROR", exitCode: 2 /* USAGE */, suggestion, retryable: false };
449778
- }
449779
- function authError(envVar, provider) {
449780
- return {
449781
- success: false,
449782
- error: `${provider} API key required.`,
449783
- code: "API_KEY_MISSING",
449784
- exitCode: 4 /* AUTH */,
449785
- suggestion: `Set ${envVar} in .env, or run: vibe setup`,
449786
- retryable: false
449787
- };
449788
- }
449789
- function apiError(msg, retryable = false) {
449790
- for (const hint of PROVIDER_ERROR_HINTS) {
449791
- if (hint.pattern.test(msg)) {
449792
- return { success: false, error: msg, code: "API_ERROR", exitCode: 5 /* API_ERROR */, suggestion: hint.suggestion, retryable: hint.retryable };
449793
- }
449794
- }
449795
- return { success: false, error: msg, code: "API_ERROR", exitCode: 5 /* API_ERROR */, suggestion: retryable ? "Retry the command." : void 0, retryable };
449796
- }
449797
- function notFoundError(path14) {
449798
- return { success: false, error: `File not found: ${path14}`, code: "NOT_FOUND", exitCode: 3 /* NOT_FOUND */, retryable: false };
449799
- }
449800
- function networkError(msg) {
449801
- return { success: false, error: msg, code: "NETWORK_ERROR", exitCode: 6 /* NETWORK */, suggestion: "Check your internet connection and retry.", retryable: true };
449802
- }
449803
- function generalError(msg, suggestion) {
449804
- return { success: false, error: msg, code: "ERROR", exitCode: 1 /* GENERAL */, suggestion, retryable: false };
449805
- }
449806
- function exitWithError(err) {
449807
- if (isJsonMode()) {
449808
- console.error(JSON.stringify(err, null, 2));
449809
- } else {
449810
- console.error(source_default.red(`
449811
- ${err.error}`));
449812
- if (err.suggestion) {
449813
- console.error(source_default.dim(` ${err.suggestion}`));
449814
- }
449815
- console.error();
449816
- }
449817
- process.exit(err.exitCode);
449818
- }
449819
- function isJsonMode() {
449820
- return process.env.VIBE_JSON_OUTPUT === "1";
449821
- }
449822
- function isQuietMode() {
449823
- return process.env.VIBE_QUIET_OUTPUT === "1";
449824
- }
449825
- function formatCost(min, max, unit) {
449826
- if (min === 0 && max === 0) return "Free";
449827
- if (min === max) return `~$${min.toFixed(2)} ${unit}`;
449828
- return `~$${min.toFixed(2)}-$${max.toFixed(2)} ${unit}`;
449829
- }
449830
- function lookupCostEstimateUpperBound(command3) {
449831
- return COST_ESTIMATES[command3]?.max ?? 0;
449832
- }
449833
- function outputSuccess(opts) {
449834
- const elapsedMs = Math.max(0, Date.now() - opts.startedAt);
449835
- const costUsd = opts.costUsd ?? (opts.dryRun ? lookupCostEstimateUpperBound(opts.command) : 0);
449836
- const envelope = {
449837
- command: opts.command,
449838
- ...opts.dryRun ? { dryRun: true } : {},
449839
- elapsedMs,
449840
- costUsd,
449841
- warnings: opts.warnings ?? [],
449842
- data: opts.data
449843
- };
449844
- if (isJsonMode()) {
449845
- const fields = process.env.VIBE_OUTPUT_FIELDS;
449846
- if (fields) {
449847
- const keys2 = fields.split(",").map((k) => k.trim());
449848
- const data = opts.data;
449849
- const filteredData = {};
449850
- for (const key2 of keys2) {
449851
- if (key2 in data) filteredData[key2] = data[key2];
449852
- }
449853
- envelope.data = filteredData;
449854
- }
449855
- console.log(JSON.stringify(envelope, null, 2));
449856
- return;
449857
- }
449858
- if (isQuietMode()) {
449859
- const data = opts.data;
449860
- const primary = data.outputPath ?? data.output ?? data.path ?? data.url ?? data.id;
449861
- if (primary !== void 0) console.log(String(primary));
449862
- }
449863
- }
449864
- function outputResult(result) {
449865
- if (result.dryRun && result.command && typeof result.command === "string") {
449866
- const cost = COST_ESTIMATES[result.command];
449867
- if (cost) {
449868
- result.estimatedCost = formatCost(cost.min, cost.max, cost.unit);
449869
- }
449870
- }
449871
- if (isJsonMode()) {
449872
- const fields = process.env.VIBE_OUTPUT_FIELDS;
449873
- if (fields) {
449874
- const keys2 = fields.split(",").map((k) => k.trim());
449875
- const filtered = {};
449876
- for (const key2 of keys2) {
449877
- if (key2 in result) filtered[key2] = result[key2];
449878
- }
449879
- if ("success" in result) filtered.success = result.success;
449880
- console.log(JSON.stringify(filtered, null, 2));
449881
- } else {
449882
- console.log(JSON.stringify(result, null, 2));
449883
- }
449884
- } else if (isQuietMode()) {
449885
- const primary = result.output ?? result.path ?? result.url ?? result.id ?? result.result;
449886
- if (primary !== void 0) console.log(String(primary));
449887
- }
449888
- }
449889
- function log(...args) {
449890
- if (!isJsonMode() && !isQuietMode()) {
449891
- console.log(...args);
449892
- }
449893
- }
449894
- function spinner(text) {
449895
- if (isJsonMode() || isQuietMode()) {
449896
- return ora({ text, isSilent: true });
449897
- }
449898
- return ora(text);
449899
- }
449900
- function suggestNext(tip) {
449901
- if (!isJsonMode() && !isQuietMode() && process.stdout.isTTY) {
449902
- console.log(source_default.dim(`
449903
- Tip: ${tip}`));
449904
- }
449905
- }
449906
- function emitDeprecationWarning(oldName, newName, removeIn) {
449907
- if (isJsonMode() || isQuietMode()) return;
449908
- if (!process.stderr.isTTY) return;
449909
- const key2 = `${oldName}\u2192${newName}`;
449910
- if (_seenDeprecations.has(key2)) return;
449911
- _seenDeprecations.add(key2);
449912
- process.stderr.write(
449913
- source_default.yellow(`[deprecated] '${oldName}' is deprecated; use '${newName}' instead. Alias will be removed in ${removeIn}.`) + "\n"
449914
- );
449915
- }
449916
- function _resetDeprecationMemoryForTesting() {
449917
- _seenDeprecations.clear();
449918
- }
449919
- function outputError(error, details) {
449920
- if (isJsonMode()) {
449921
- console.error(JSON.stringify({ success: false, error, ...details }, null, 2));
449922
- } else {
449923
- console.error(error);
449924
- }
449925
- }
449926
- var ExitCode, PROVIDER_ERROR_HINTS, COST_ESTIMATES, _seenDeprecations;
449927
- var init_output = __esm({
449928
- "../cli/src/commands/output.ts"() {
449929
- "use strict";
449930
- init_source();
449931
- init_ora();
449932
- ExitCode = /* @__PURE__ */ ((ExitCode2) => {
449933
- ExitCode2[ExitCode2["SUCCESS"] = 0] = "SUCCESS";
449934
- ExitCode2[ExitCode2["GENERAL"] = 1] = "GENERAL";
449935
- ExitCode2[ExitCode2["USAGE"] = 2] = "USAGE";
449936
- ExitCode2[ExitCode2["NOT_FOUND"] = 3] = "NOT_FOUND";
449937
- ExitCode2[ExitCode2["AUTH"] = 4] = "AUTH";
449938
- ExitCode2[ExitCode2["API_ERROR"] = 5] = "API_ERROR";
449939
- ExitCode2[ExitCode2["NETWORK"] = 6] = "NETWORK";
449940
- return ExitCode2;
449941
- })(ExitCode || {});
449942
- PROVIDER_ERROR_HINTS = [
449943
- // Billing (must precede the 429 rate-limit pattern)
449944
- { pattern: /402|payment.*required|billing|INSUFFICIENT_BALANCE|insufficient.*(credit|funds|balance)|balance.*(not.*enough|insufficient)|credits?.*exhausted|account.*balance/i, suggestion: "Account balance or credits exhausted. Top up at the provider dashboard, or try -p <other-provider>.", retryable: false },
449945
- // Rate limits / quota
449946
- { pattern: /429|rate.?limit|too many requests/i, suggestion: "Rate limited. Wait 30-60 seconds and retry, or check your plan's rate limits.", retryable: true },
449947
- { pattern: /RESOURCE_EXHAUSTED|quota.*exceeded|requests.*per.*(minute|day)/i, suggestion: "Quota exceeded. Wait for the quota window to reset, or upgrade your plan. Consider -p <other-provider> to use a different provider.", retryable: true },
449948
- // Auth
449949
- { pattern: /401|unauthorized|(invalid|incorrect).*api.?key|invalid_api_key|authentication.*(failed|error)|missing.*api.?key|did not start with 'key_'/i, suggestion: "API key is invalid or expired. Run 'vibe setup' to update, or check the key at the provider's dashboard.", retryable: false },
449950
- { pattern: /403|forbidden|permission.*denied/i, suggestion: "Access denied. Your API key may lack required permissions, or the feature requires a paid plan.", retryable: false },
449951
- // Server
449952
- { pattern: /500|internal.*error|server.*error/i, suggestion: "Provider server error. Retry in a few minutes.", retryable: true },
449953
- { pattern: /503|service.*unavailable|overloaded|overloaded_error/i, suggestion: "Provider is temporarily overloaded. Retry in 1-2 minutes, or switch provider with -p.", retryable: true },
449954
- { pattern: /timeout|timed?\s*out|ETIMEDOUT|ECONNRESET|socket.*hang.?up/i, suggestion: "Request timed out. The provider may be slow. Retry, or try a different provider with -p flag.", retryable: true },
449955
- // Content policy
449956
- { pattern: /content.*(policy|filter)|safety|moderation|blocked.*(by|due)|content_policy_violation|restricted.*content/i, suggestion: "Content was blocked by the provider's safety filter. Rephrase your prompt to avoid sensitive terms.", retryable: false },
449957
- // Model / context
449958
- { pattern: /context_length_exceeded|maximum.*context.*length|token.*limit.*exceeded|prompt.*too.*long/i, suggestion: "Input exceeds the model's context window. Shorten the prompt, or use a model with larger context (run 'vibe schema <command>' for options).", retryable: false },
449959
- { pattern: /model.*not.*found|invalid.*model|unknown.*model|model_not_found/i, suggestion: "The specified model is unavailable. Check 'vibe schema <command>' for valid model options.", retryable: false },
449960
- // Provider-specific
449961
- { pattern: /voice.*not.*found|voice_not_found|invalid.*voice.?id/i, suggestion: "Voice ID not found. Run 'vibe audio list-voices' to list available voices, then pass --voice <id>.", retryable: false },
449962
- { pattern: /character.*(count|limit).*exceeded|invalid_character_count/i, suggestion: "Text exceeds the TTS provider's character limit. Shorten the text or split into chunks.", retryable: false },
449963
- { pattern: /invalid.*aspect.*ratio|unsupported.*aspect.*ratio|unsupported.*resolution/i, suggestion: "This aspect ratio or resolution isn't supported by the chosen model. Check 'vibe schema <command>' for supported values.", retryable: false },
449964
- { pattern: /invalid.*file.*format|unsupported.*(format|codec)|unsupported.*media.?type/i, suggestion: "Input file format not supported. Convert to MP4/MP3/PNG first with 'vibe export' or 'ffmpeg'.", retryable: false },
449965
- { pattern: /region.*(restriction|not.*supported|unavailable)|geo.?blocked/i, suggestion: "Provider unavailable in your region. Try -p <other-provider>, or use a supported region.", retryable: false },
449966
- { pattern: /task.*(not.*found|expired)|job.*(not.*found|expired)/i, suggestion: "The async task expired or was never created. Re-run the command to start a new task.", retryable: true }
449967
- ];
449968
- COST_ESTIMATES = {
449969
- // Free
449970
- "detect scenes": { min: 0, max: 0, unit: "free" },
449971
- "detect silence": { min: 0, max: 0, unit: "free" },
449972
- "detect beats": { min: 0, max: 0, unit: "free" },
449973
- "edit silence-cut": { min: 0, max: 0, unit: "free" },
449974
- "edit fade": { min: 0, max: 0, unit: "free" },
449975
- "edit noise-reduce": { min: 0, max: 0, unit: "free" },
449976
- "edit reframe": { min: 0, max: 0, unit: "free" },
449977
- "edit interpolate": { min: 0, max: 0, unit: "free" },
449978
- "edit upscale-video": { min: 0, max: 0, unit: "free" },
449979
- // Low
449980
- "analyze media": { min: 0.01, max: 0.05, unit: "per call" },
449981
- "analyze video": { min: 0.01, max: 0.1, unit: "per video" },
449982
- "analyze review": { min: 0.01, max: 0.1, unit: "per video" },
449983
- "generate image": { min: 0.01, max: 0.07, unit: "per image" },
449984
- "generate thumbnail": { min: 0.01, max: 0.05, unit: "per image" },
449985
- "generate storyboard": { min: 0.01, max: 0.05, unit: "per call" },
449986
- "ai transcribe": { min: 0.01, max: 0.1, unit: "per minute" },
449987
- "audio transcribe": { min: 0.01, max: 0.1, unit: "per minute" },
449988
- "edit caption": { min: 0.01, max: 0.1, unit: "per video" },
449989
- "edit jump-cut": { min: 0.01, max: 0.1, unit: "per video" },
449990
- "edit translate-srt": { min: 0.01, max: 0.05, unit: "per file" },
449991
- "edit animated-caption": { min: 0.01, max: 0.1, unit: "per video" },
449992
- // Medium
449993
- "generate speech": { min: 0.05, max: 0.3, unit: "per request" },
449994
- "generate sound-effect": { min: 0.05, max: 0.2, unit: "per request" },
449995
- "generate music": { min: 0.05, max: 0.5, unit: "per request" },
449996
- "generate motion": { min: 0.01, max: 0.1, unit: "per generation" },
449997
- "edit grade": { min: 0.01, max: 0.05, unit: "per video" },
449998
- "edit speed-ramp": { min: 0.05, max: 0.15, unit: "per video" },
449999
- "edit text-overlay": { min: 0, max: 0.05, unit: "per video" },
450000
- // High
450001
- "generate video": { min: 0.5, max: 5, unit: "per video" },
450002
- "edit image": { min: 0.05, max: 0.5, unit: "per edit" },
450003
- // Very High
450004
- "pipeline highlights": { min: 0.05, max: 1, unit: "per analysis" },
450005
- "pipeline auto-shorts": { min: 0.1, max: 2, unit: "per batch" },
450006
- "pipeline animated-caption": { min: 0.01, max: 0.1, unit: "per video" },
450007
- "pipeline regenerate-scene": { min: 0.5, max: 5, unit: "per scene" },
450008
- // Scene composition (v0.59+) — per-beat composer call ≈ $0.06 (PR #111
450009
- // pre-flight). Range covers a 1-beat preview to a 10-beat long-form.
450010
- "compose scenes with skills": { min: 0.05, max: 1.5, unit: "per pipeline" }
450011
- };
450012
- _seenDeprecations = /* @__PURE__ */ new Set();
450013
- }
450014
- });
450015
-
450016
450013
  // ../cli/src/utils/subtitle.ts
450017
450014
  function detectFormat(outputPath, explicitFormat) {
450018
450015
  if (explicitFormat) {
@@ -450120,7 +450117,7 @@ __export(ai_audio_exports, {
450120
450117
  executeTranscribe: () => executeTranscribe,
450121
450118
  executeVoiceClone: () => executeVoiceClone
450122
450119
  });
450123
- import { resolve as resolve24, dirname as dirname18, basename as basename7, extname as extname5 } from "node:path";
450120
+ import { resolve as resolve24, dirname as dirname18, basename as basename6, extname as extname5 } from "node:path";
450124
450121
  import { readFile as readFile11, writeFile as writeFile11 } from "node:fs/promises";
450125
450122
  import { existsSync as existsSync29 } from "node:fs";
450126
450123
  async function executeTranscribe(options) {
@@ -450303,7 +450300,7 @@ ${segmentTexts}`,
450303
450300
  }
450304
450301
  const combinedBuffer = Buffer.concat(dubbedBuffers);
450305
450302
  const outputExt = isVideo ? ".mp3" : extname5(absPath);
450306
- const defaultOutputPath2 = resolve24(dirname18(absPath), `${basename7(absPath, extname5(absPath))}-${language}${outputExt}`);
450303
+ const defaultOutputPath2 = resolve24(dirname18(absPath), `${basename6(absPath, extname5(absPath))}-${language}${outputExt}`);
450307
450304
  const finalOutputPath = resolve24(process.cwd(), output3 || defaultOutputPath2);
450308
450305
  await writeFile11(finalOutputPath, combinedBuffer);
450309
450306
  if (isVideo && audioPath !== absPath) {
@@ -450340,7 +450337,7 @@ async function executeDuck(options) {
450340
450337
  const absVoicePath = resolve24(process.cwd(), voicePath);
450341
450338
  if (!existsSync29(absMusicPath)) return { success: false, error: `Music file not found: ${absMusicPath}` };
450342
450339
  if (!existsSync29(absVoicePath)) return { success: false, error: `Voice file not found: ${absVoicePath}` };
450343
- const defaultOutput = resolve24(dirname18(absMusicPath), `${basename7(absMusicPath, extname5(absMusicPath))}-ducked${extname5(absMusicPath)}`);
450340
+ const defaultOutput = resolve24(dirname18(absMusicPath), `${basename6(absMusicPath, extname5(absMusicPath))}-ducked${extname5(absMusicPath)}`);
450344
450341
  const outputPath = resolve24(process.cwd(), output3 || defaultOutput);
450345
450342
  const filter4 = `[1:a]asplit=2[sc][mix];[0:a][sc]sidechaincompress=threshold=${threshold}dB:ratio=${ratio}:attack=${attack}:release=${release}[ducked];[ducked][mix]amix=inputs=2:duration=longest`;
450346
450343
  await execSafe("ffmpeg", [
@@ -451626,7 +451623,7 @@ var init_remotion = __esm({
451626
451623
  // ../cli/src/commands/_shared/edit/caption.ts
451627
451624
  import { existsSync as existsSync34 } from "node:fs";
451628
451625
  import { readFile as readFile14, writeFile as writeFile14, mkdir as mkdir13 } from "node:fs/promises";
451629
- import { dirname as dirname19, basename as basename8, extname as extname6, join as join29 } from "node:path";
451626
+ import { dirname as dirname19, basename as basename7, extname as extname6, join as join29 } from "node:path";
451630
451627
  function getCaptionForceStyle(style, fontSize, fontColor, position) {
451631
451628
  const alignment = position === "top" ? 8 : position === "center" ? 5 : 2;
451632
451629
  const marginV = position === "center" ? 0 : 30;
@@ -451745,7 +451742,7 @@ async function executeCaption(options) {
451745
451742
  const outputDir2 = dirname19(outputPath);
451746
451743
  const outputSrtPath2 = join29(
451747
451744
  outputDir2,
451748
- basename8(outputPath, extname6(outputPath)) + ".srt"
451745
+ basename7(outputPath, extname6(outputPath)) + ".srt"
451749
451746
  );
451750
451747
  await writeFile14(outputSrtPath2, srtContent);
451751
451748
  return { success: false, error: `${remotionErr}
@@ -451784,7 +451781,7 @@ SRT saved to: ${outputSrtPath2}` };
451784
451781
  const outputDir2 = dirname19(outputPath);
451785
451782
  const outputSrtPath2 = join29(
451786
451783
  outputDir2,
451787
- basename8(outputPath, extname6(outputPath)) + ".srt"
451784
+ basename7(outputPath, extname6(outputPath)) + ".srt"
451788
451785
  );
451789
451786
  await writeFile14(outputSrtPath2, srtContent);
451790
451787
  return {
@@ -451797,7 +451794,7 @@ SRT saved to: ${outputSrtPath2}`
451797
451794
  const outputDir = dirname19(outputPath);
451798
451795
  const outputSrtPath = join29(
451799
451796
  outputDir,
451800
- basename8(outputPath, extname6(outputPath)) + ".srt"
451797
+ basename7(outputPath, extname6(outputPath)) + ".srt"
451801
451798
  );
451802
451799
  await writeFile14(outputSrtPath, srtContent);
451803
451800
  return {
@@ -452403,7 +452400,7 @@ var init_validate = __esm({
452403
452400
  });
452404
452401
 
452405
452402
  // ../cli/src/commands/ai-edit-cli.ts
452406
- import { resolve as resolve27, extname as extname7, basename as basename9 } from "node:path";
452403
+ import { resolve as resolve27, extname as extname7, basename as basename8 } from "node:path";
452407
452404
  import { existsSync as existsSync39 } from "node:fs";
452408
452405
  function registerEditCommands(aiCommand) {
452409
452406
  aiCommand.command("silence-cut").alias("sc").description("Remove silent segments from video (FFmpeg default, or Gemini for smart detection)").argument("<video>", "Video file path").option("-o, --output <path>", "Output file path (default: <name>-cut.<ext>)").option("-n, --noise <dB>", "Silence threshold in dB (default: -30)", "-30").option("-d, --min-duration <seconds>", "Minimum silence duration to cut (default: 0.5)", "0.5").option("--padding <seconds>", "Padding around non-silent segments (default: 0.1)", "0.1").option("--analyze-only", "Only detect silence, don't cut").option("--use-gemini", "Use Gemini Video Understanding for context-aware silence detection").option("-m, --model <model>", "Gemini model (default: flash)").option("--low-res", "Low resolution mode for longer videos (Gemini only)").option("-k, --api-key <key>", "Google API key override (or set GOOGLE_API_KEY env)").option("--dry-run", "Preview parameters without executing").addHelpText("after", `
@@ -452428,7 +452425,7 @@ No API key needed (FFmpeg only). Use --use-gemini for smart detection (requires
452428
452425
  exitWithError(generalError("FFmpeg not found. Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux). Run `vibe doctor` for details."));
452429
452426
  }
452430
452427
  const ext = extname7(videoPath);
452431
- const name = basename9(videoPath, ext);
452428
+ const name = basename8(videoPath, ext);
452432
452429
  const outputPath = options.output || `${name}-cut${ext}`;
452433
452430
  const useGemini = options.useGemini || false;
452434
452431
  if (options.dryRun) {
@@ -452551,7 +452548,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452551
452548
  exitWithError(authError("OPENAI_API_KEY", "OpenAI"));
452552
452549
  }
452553
452550
  const ext = extname7(videoPath);
452554
- const name = basename9(videoPath, ext);
452551
+ const name = basename8(videoPath, ext);
452555
452552
  const outputPath = options.output || `${name}-captioned${ext}`;
452556
452553
  const spinner2 = ora("Starting caption process...").start();
452557
452554
  const result = await executeCaption({
@@ -452625,7 +452622,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452625
452622
  return;
452626
452623
  }
452627
452624
  const ext = extname7(inputPath);
452628
- const name = basename9(inputPath, ext);
452625
+ const name = basename8(inputPath, ext);
452629
452626
  const outputPath = options.output || `${name}-denoised${ext}`;
452630
452627
  const spinner2 = ora("Applying noise reduction...").start();
452631
452628
  const result = await executeNoiseReduce({
@@ -452693,7 +452690,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452693
452690
  return;
452694
452691
  }
452695
452692
  const ext = extname7(videoPath);
452696
- const name = basename9(videoPath, ext);
452693
+ const name = basename8(videoPath, ext);
452697
452694
  const outputPath = options.output || `${name}-faded${ext}`;
452698
452695
  const spinner2 = ora("Applying fade effects...").start();
452699
452696
  const result = await executeFade({
@@ -452771,7 +452768,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452771
452768
  exitWithError(authError(envKey, providerName));
452772
452769
  }
452773
452770
  const ext = extname7(srtPath);
452774
- const name = basename9(srtPath, ext);
452771
+ const name = basename8(srtPath, ext);
452775
452772
  const outputPath = options.output || `${name}-${options.target}${ext}`;
452776
452773
  const spinner2 = ora(`Translating to ${options.target}...`).start();
452777
452774
  const result = await executeTranslateSrt({
@@ -452849,7 +452846,7 @@ Requires: OPENAI_API_KEY (Whisper transcription) + FFmpeg`).action(async (videoP
452849
452846
  exitWithError(authError("OPENAI_API_KEY", "OpenAI"));
452850
452847
  }
452851
452848
  const ext = extname7(videoPath);
452852
- const name = basename9(videoPath, ext);
452849
+ const name = basename8(videoPath, ext);
452853
452850
  const outputPath = options.output || `${name}-jumpcut${ext}`;
452854
452851
  const fillers = options.fillers ? options.fillers.split(",").map((f) => f.trim()) : void 0;
452855
452852
  const spinner2 = ora("Transcribing with word-level timestamps...").start();
@@ -456838,7 +456835,7 @@ var init_music = __esm({
456838
456835
  });
456839
456836
 
456840
456837
  // ../cli/src/commands/generate/thumbnail.ts
456841
- import { resolve as resolve43, dirname as dirname26, basename as basename11, extname as extname9 } from "node:path";
456838
+ import { resolve as resolve43, dirname as dirname26, basename as basename10, extname as extname9 } from "node:path";
456842
456839
  import { existsSync as existsSync47 } from "node:fs";
456843
456840
  import { writeFile as writeFile27, mkdir as mkdir19 } from "node:fs/promises";
456844
456841
  function registerThumbnailCommand(parent) {
@@ -456863,7 +456860,7 @@ function registerThumbnailCommand(parent) {
456863
456860
  );
456864
456861
  }
456865
456862
  const apiKey2 = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
456866
- const name = basename11(options.bestFrame, extname9(options.bestFrame));
456863
+ const name = basename10(options.bestFrame, extname9(options.bestFrame));
456867
456864
  const outputPath = options.output || `${name}-thumbnail.png`;
456868
456865
  const spinner3 = ora("Analyzing video for best frame...").start();
456869
456866
  const result2 = await executeThumbnailBestFrame({
@@ -458928,7 +458925,7 @@ var init_dist3 = __esm({
458928
458925
 
458929
458926
  // ../cli/src/commands/_shared/video-utils.ts
458930
458927
  import { writeFile as writeFile31, unlink as unlink5, rename as rename5 } from "node:fs/promises";
458931
- import { resolve as resolve47, basename as basename12 } from "node:path";
458928
+ import { resolve as resolve47, basename as basename11 } from "node:path";
458932
458929
  function sleep(ms) {
458933
458930
  return new Promise((resolve64) => setTimeout(resolve64, ms));
458934
458931
  }
@@ -458962,7 +458959,7 @@ async function extendVideoToTarget(videoPath, targetDuration, outputDir, sceneLa
458962
458959
  const actualDuration = await getVideoDuration(videoPath);
458963
458960
  if (actualDuration >= targetDuration - 0.1) return;
458964
458961
  const ratio = targetDuration / actualDuration;
458965
- const extendedPath = resolve47(outputDir, `${basename12(videoPath, ".mp4")}-extended.mp4`);
458962
+ const extendedPath = resolve47(outputDir, `${basename11(videoPath, ".mp4")}-extended.mp4`);
458966
458963
  if (ratio > 1.4 && options?.kling && options?.videoId) {
458967
458964
  try {
458968
458965
  options.onProgress?.(`${sceneLabel}: Extending via Kling API...`);
@@ -458980,17 +458977,17 @@ async function extendVideoToTarget(videoPath, targetDuration, outputDir, sceneLa
458980
458977
  if (waitResult.status === "completed" && waitResult.videoUrl) {
458981
458978
  const extendedVideoPath = resolve47(
458982
458979
  outputDir,
458983
- `${basename12(videoPath, ".mp4")}-kling-ext.mp4`
458980
+ `${basename11(videoPath, ".mp4")}-kling-ext.mp4`
458984
458981
  );
458985
458982
  const buffer = await downloadVideo(waitResult.videoUrl);
458986
458983
  await writeFile31(extendedVideoPath, buffer);
458987
458984
  const concatPath = resolve47(
458988
458985
  outputDir,
458989
- `${basename12(videoPath, ".mp4")}-concat.mp4`
458986
+ `${basename11(videoPath, ".mp4")}-concat.mp4`
458990
458987
  );
458991
458988
  const listPath = resolve47(
458992
458989
  outputDir,
458993
- `${basename12(videoPath, ".mp4")}-concat.txt`
458990
+ `${basename11(videoPath, ".mp4")}-concat.txt`
458994
458991
  );
458995
458992
  await writeFile31(
458996
458993
  listPath,
@@ -460521,7 +460518,7 @@ __export(detect_exports, {
460521
460518
  executeDetectSilence: () => executeDetectSilence
460522
460519
  });
460523
460520
  import { readFile as readFile28, writeFile as writeFile36 } from "node:fs/promises";
460524
- import { resolve as resolve53, basename as basename14 } from "node:path";
460521
+ import { resolve as resolve53, basename as basename13 } from "node:path";
460525
460522
  async function executeDetectScenes(options) {
460526
460523
  try {
460527
460524
  if (!commandExists("ffmpeg")) {
@@ -460770,7 +460767,7 @@ var init_detect = __esm({
460770
460767
  let source3 = project.getSources().find((s) => s.url === absPath);
460771
460768
  if (!source3) {
460772
460769
  source3 = project.addSource({
460773
- name: basename14(absPath),
460770
+ name: basename13(absPath),
460774
460771
  type: "video",
460775
460772
  url: absPath,
460776
460773
  duration: totalDuration
@@ -460974,8 +460971,8 @@ var init_detect = __esm({
460974
460971
 
460975
460972
  // ../cli/src/pipeline/renderers/html-clips.ts
460976
460973
  function relAsset(url) {
460977
- const basename18 = url.split("/").pop() ?? url;
460978
- return `assets/${basename18}`;
460974
+ const basename17 = url.split("/").pop() ?? url;
460975
+ return `assets/${basename17}`;
460979
460976
  }
460980
460977
  function buildClipElements(state) {
460981
460978
  return state.clips.map((clip) => {
@@ -461505,13 +461502,13 @@ init_scene_project();
461505
461502
  init_esm();
461506
461503
  init_source();
461507
461504
  init_ora();
461508
- var import_yaml5 = __toESM(require_dist(), 1);
461505
+ var import_yaml4 = __toESM(require_dist(), 1);
461509
461506
  init_dist();
461510
461507
  init_tts_resolve();
461511
461508
  init_scene_project();
461512
- import { basename as basename6, resolve as resolve21, relative as relative7, dirname as dirname17 } from "node:path";
461513
- import { mkdir as mkdir10, readFile as readFile10, writeFile as writeFile10, access as access4, copyFile as copyFile2 } from "node:fs/promises";
461514
- import { existsSync as existsSync28 } from "node:fs";
461509
+ import { resolve as resolve17, relative as relative6, dirname as dirname14 } from "node:path";
461510
+ import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile9, access as access4, copyFile as copyFile2 } from "node:fs/promises";
461511
+ import { existsSync as existsSync25 } from "node:fs";
461515
461512
 
461516
461513
  // ../cli/src/commands/_shared/scene-html-emit.ts
461517
461514
  var SCENE_PRESETS = [
@@ -461894,8 +461891,6 @@ function readRootDims(rootHtml) {
461894
461891
 
461895
461892
  // ../cli/src/commands/scene.ts
461896
461893
  init_scene_lint();
461897
- init_scene_render();
461898
- init_scene_build();
461899
461894
  init_output();
461900
461895
  init_api_key();
461901
461896
  init_audio();
@@ -461904,9 +461899,9 @@ init_agent_host_detect();
461904
461899
  // ../cli/src/commands/_shared/install-skill.ts
461905
461900
  init_bundle_content();
461906
461901
  init_bundle();
461907
- import { mkdir as mkdir9, writeFile as writeFile9 } from "node:fs/promises";
461908
- import { existsSync as existsSync27 } from "node:fs";
461909
- import { dirname as dirname16, join as join26, relative as relative6, resolve as resolve20 } from "node:path";
461902
+ import { mkdir as mkdir6, writeFile as writeFile7 } from "node:fs/promises";
461903
+ import { existsSync as existsSync21 } from "node:fs";
461904
+ import { dirname as dirname12, join as join21, relative as relative4, resolve as resolve14 } from "node:path";
461910
461905
  function universalFiles() {
461911
461906
  return [
461912
461907
  { relPath: "SKILL.md", content: SKILL_MD },
@@ -461956,21 +461951,21 @@ function selectHostFiles(hosts) {
461956
461951
  return out;
461957
461952
  }
461958
461953
  async function installHyperframesSkill(opts) {
461959
- const projectDir = resolve20(opts.projectDir);
461954
+ const projectDir = resolve14(opts.projectDir);
461960
461955
  const force = opts.force ?? false;
461961
461956
  const dryRun = opts.dryRun ?? false;
461962
461957
  const files = [...universalFiles(), ...selectHostFiles(opts.hosts)];
461963
461958
  const actions = [];
461964
- if (!dryRun && !existsSync27(projectDir)) {
461965
- await mkdir9(projectDir, { recursive: true });
461959
+ if (!dryRun && !existsSync21(projectDir)) {
461960
+ await mkdir6(projectDir, { recursive: true });
461966
461961
  }
461967
461962
  for (const file of files) {
461968
- const absPath = join26(projectDir, file.relPath);
461969
- const exists = existsSync27(absPath);
461963
+ const absPath = join21(projectDir, file.relPath);
461964
+ const exists = existsSync21(absPath);
461970
461965
  const bytes = Buffer.byteLength(file.content, "utf-8");
461971
461966
  if (exists && !force) {
461972
461967
  actions.push({
461973
- path: relative6(projectDir, absPath) || file.relPath,
461968
+ path: relative4(projectDir, absPath) || file.relPath,
461974
461969
  status: dryRun ? "would-skip-exists" : "skipped-exists",
461975
461970
  bytes
461976
461971
  });
@@ -461980,8 +461975,8 @@ async function installHyperframesSkill(opts) {
461980
461975
  actions.push({ path: file.relPath, status: "would-write", bytes });
461981
461976
  continue;
461982
461977
  }
461983
- await mkdir9(dirname16(absPath), { recursive: true });
461984
- await writeFile9(absPath, file.content, "utf-8");
461978
+ await mkdir6(dirname12(absPath), { recursive: true });
461979
+ await writeFile7(absPath, file.content, "utf-8");
461985
461980
  actions.push({ path: file.relPath, status: "wrote", bytes });
461986
461981
  }
461987
461982
  return {
@@ -461999,14 +461994,6 @@ function deriveInstallHosts(detected) {
461999
461994
 
462000
461995
  // ../cli/src/commands/scene.ts
462001
461996
  init_compose_prompts();
462002
- var VALID_ASPECTS2 = ["16:9", "9:16", "1:1", "4:5"];
462003
- var VALID_SCENE_INIT_PROFILES = ["minimal", "agent", "full"];
462004
- function validateAspect(value) {
462005
- if (!VALID_ASPECTS2.includes(value)) {
462006
- exitWithError(usageError(`Invalid aspect ratio: ${value}`, `Valid: ${VALID_ASPECTS2.join(", ")}`));
462007
- }
462008
- return value;
462009
- }
462010
461997
  function validateDuration(value) {
462011
461998
  const n = parseFloat(value);
462012
461999
  if (!Number.isFinite(n) || n <= 0 || n > 3600) {
@@ -462020,23 +462007,6 @@ function validatePreset(value) {
462020
462007
  }
462021
462008
  return value;
462022
462009
  }
462023
- function validateVisualStyle(value) {
462024
- const found = getVisualStyle(value);
462025
- if (!found) {
462026
- exitWithError(
462027
- usageError(
462028
- `Unknown visual style: ${value}`,
462029
- `Valid: ${visualStyleNames()}. Browse details with \`vibe scene styles\`.`
462030
- )
462031
- );
462032
- }
462033
- return found;
462034
- }
462035
- function formatSceneInitProfile(profile) {
462036
- if (profile === "minimal") return "authoring files only; build will add render scaffold when needed";
462037
- if (profile === "agent") return "authoring files plus local composition rules for host agents";
462038
- return "complete authoring, agent, and render scaffold";
462039
- }
462040
462010
  var sceneCommand = new Command("scene").description("Lower-level scene authoring (add, lint, styles). For project flow use `vibe init` / `vibe build` / `vibe render`.").addHelpText("after", `
462041
462011
  Examples:
462042
462012
  $ vibe scene add intro --style announcement \\
@@ -462052,138 +462022,6 @@ For the project flow (init / build / render), use the top-level commands.
462052
462022
  The \`scene init\`, \`scene build\`, and \`scene render\` legacy aliases
462053
462023
  are still callable but hidden from this help \u2014 they will be removed in v1.0.
462054
462024
  Run 'vibe schema scene.<command>' for structured parameter info.`);
462055
- sceneCommand.command("init", { hidden: true }).description("Scaffold a new scene project (or safely augment an existing project) [legacy \u2014 prefer `vibe init`]").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) => {
462056
- const startedAt = Date.now();
462057
- const aspect = validateAspect(options.ratio);
462058
- const duration = validateDuration(options.duration);
462059
- const name = options.name ?? basename6(dir.replace(/\/+$/, ""));
462060
- const visualStyle = options.visualStyle ? validateVisualStyle(options.visualStyle) : void 0;
462061
- const profile = String(options.profile ?? "agent");
462062
- if (!isSceneScaffoldProfile(profile)) {
462063
- exitWithError(usageError(`Invalid --profile: ${profile}`, `Must be one of: ${VALID_SCENE_INIT_PROFILES.join(", ")}`));
462064
- }
462065
- const groups = describeSceneScaffold({ dir, profile });
462066
- if (options.dryRun) {
462067
- if (!isJsonMode() && !isQuietMode()) {
462068
- printSceneInitDryRun({ dir, name, aspect, duration, visualStyleName: visualStyle?.name ?? null, profile, groups });
462069
- return;
462070
- }
462071
- outputSuccess({
462072
- command: "scene init",
462073
- startedAt,
462074
- dryRun: true,
462075
- data: {
462076
- params: {
462077
- dir,
462078
- name,
462079
- aspect,
462080
- duration,
462081
- visualStyle: visualStyle?.name ?? null,
462082
- profile
462083
- },
462084
- groups
462085
- }
462086
- });
462087
- return;
462088
- }
462089
- const spinner2 = isJsonMode() || isQuietMode() ? null : ora(`Scaffolding scene project at ${dir}...`).start();
462090
- try {
462091
- const result = await scaffoldSceneProject({ dir, name, aspect, duration, visualStyle, profile });
462092
- const detectedIds = detectedAgentHosts().map((h) => h.id);
462093
- const skillHosts = deriveInstallHosts(detectedIds);
462094
- const projectAbs = resolve21(dir);
462095
- const skillResult = profile === "agent" || profile === "full" ? await installHyperframesSkill({
462096
- projectDir: projectAbs,
462097
- hosts: skillHosts
462098
- }) : { success: true, files: [], bundleVersion: "not-installed" };
462099
- if (isJsonMode() || isQuietMode()) {
462100
- outputSuccess({
462101
- command: "scene init",
462102
- startedAt,
462103
- data: {
462104
- dir,
462105
- name,
462106
- aspect,
462107
- duration,
462108
- visualStyle: visualStyle?.name ?? null,
462109
- profile,
462110
- created: result.created,
462111
- merged: result.merged,
462112
- skipped: result.skipped,
462113
- groups: result.groups,
462114
- skillFiles: skillResult.files,
462115
- skillBundleVersion: skillResult.bundleVersion
462116
- }
462117
- });
462118
- return;
462119
- }
462120
- spinner2?.succeed(source_default.green(`Video project ready: ${dir}`));
462121
- console.log();
462122
- console.log(source_default.bold.cyan("Edit first"));
462123
- console.log(source_default.dim("\u2500".repeat(60)));
462124
- console.log(` ${source_default.bold("STORYBOARD.md")} ${source_default.dim("# beats: narration, backdrop, minimum duration")}`);
462125
- console.log(` ${source_default.bold("DESIGN.md")} ${source_default.dim("# palette, typography, motion rules")}`);
462126
- console.log();
462127
- console.log(source_default.bold.cyan("Profile"));
462128
- console.log(source_default.dim("\u2500".repeat(60)));
462129
- console.log(` ${source_default.bold(profile)} ${source_default.dim(formatSceneInitProfile(profile))}`);
462130
- console.log();
462131
- console.log(source_default.bold.cyan("Files"));
462132
- console.log(source_default.dim("\u2500".repeat(60)));
462133
- for (const f of result.created) console.log(source_default.green(" +"), f);
462134
- for (const f of result.merged) console.log(source_default.yellow(" ~"), f, source_default.dim("(merged)"));
462135
- for (const f of result.skipped) console.log(source_default.dim(" \xB7"), f, source_default.dim("(kept existing)"));
462136
- const skillWritten = skillResult.files.filter((f) => f.status === "wrote");
462137
- const skillSkipped = skillResult.files.filter((f) => f.status === "skipped-exists");
462138
- if (skillWritten.length + skillSkipped.length > 0) {
462139
- console.log();
462140
- console.log(source_default.bold.cyan("Composition rules"));
462141
- console.log(source_default.dim("\u2500".repeat(60)));
462142
- for (const f of skillWritten) console.log(source_default.green(" +"), f.path);
462143
- for (const f of skillSkipped) console.log(source_default.dim(" \xB7"), f.path, source_default.dim("(kept existing)"));
462144
- console.log(source_default.dim(` Bundle: ${skillResult.bundleVersion}`));
462145
- }
462146
- console.log();
462147
- console.log(source_default.bold.cyan("Next steps"));
462148
- console.log(source_default.dim("\u2500".repeat(60)));
462149
- if (visualStyle) {
462150
- console.log(` ${source_default.dim("DESIGN.md seeded with")} ${source_default.bold(visualStyle.name)} ${source_default.dim("\u2014 review and customise.")}`);
462151
- } else {
462152
- console.log(` ${source_default.cyan("vibe scene styles")} ${source_default.dim("# pick a named style for DESIGN.md")}`);
462153
- }
462154
- if (profile === "agent" || profile === "full") {
462155
- 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.")}`);
462156
- } else {
462157
- console.log(` ${source_default.cyan("vibe scene install-skill")} ${source_default.dim("# add agent authoring rules later")}`);
462158
- }
462159
- console.log(` ${source_default.cyan("vibe scene add")} <name> ${source_default.dim("# fallback: 5-preset emit (no agent)")}`);
462160
- console.log(` ${source_default.cyan("vibe build")} ${source_default.dim("# build STORYBOARD.md into scenes/assets")}`);
462161
- console.log(` ${source_default.cyan("vibe render")} ${source_default.dim("# render to video")}`);
462162
- } catch (error) {
462163
- spinner2?.fail("Failed to scaffold scene project");
462164
- const msg = error instanceof Error ? error.message : String(error);
462165
- exitWithError(generalError(`Failed to scaffold: ${msg}`));
462166
- }
462167
- });
462168
- function printSceneInitDryRun(opts) {
462169
- console.log();
462170
- console.log(source_default.bold.cyan("VibeFrame Scene Init - dry run"));
462171
- console.log(source_default.dim("-".repeat(60)));
462172
- console.log(` Project: ${source_default.bold(opts.dir)}`);
462173
- console.log(` Name: ${source_default.bold(opts.name)}`);
462174
- console.log(` Profile: ${source_default.bold(opts.profile)} ${source_default.dim(formatSceneInitProfile(opts.profile))}`);
462175
- console.log(` Aspect: ${opts.aspect}`);
462176
- console.log(` Duration: ${opts.duration}s`);
462177
- console.log(` Visual style: ${opts.visualStyleName ?? "none"}`);
462178
- console.log();
462179
- console.log(source_default.bold.cyan("Files that would be prepared"));
462180
- console.log(source_default.dim("-".repeat(60)));
462181
- for (const file of opts.groups.authoring) console.log(` authoring ${file}`);
462182
- for (const file of opts.groups.agent) console.log(` agent ${file}`);
462183
- for (const file of opts.groups.render) console.log(` render ${file}`);
462184
- console.log();
462185
- console.log(source_default.dim("No files were written."));
462186
- }
462187
462025
  var VALID_INSTALL_SKILL_HOSTS = ["claude-code", "cursor", "auto", "all"];
462188
462026
  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) => {
462189
462027
  const startedAt = Date.now();
@@ -462191,7 +462029,7 @@ sceneCommand.command("install-skill").description("Install the Hyperframes skill
462191
462029
  if (!VALID_INSTALL_SKILL_HOSTS.includes(hostFlag)) {
462192
462030
  exitWithError(usageError(`Invalid --host: ${hostFlag}`, `Valid: ${VALID_INSTALL_SKILL_HOSTS.join(", ")}`));
462193
462031
  }
462194
- const projectDir = resolve21(projectDirArg);
462032
+ const projectDir = resolve17(projectDirArg);
462195
462033
  const hosts = (() => {
462196
462034
  if (hostFlag === "all") return ["all"];
462197
462035
  if (hostFlag === "auto") {
@@ -462239,7 +462077,7 @@ sceneCommand.command("install-skill").description("Install the Hyperframes skill
462239
462077
  });
462240
462078
  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) => {
462241
462079
  const startedAt = Date.now();
462242
- const projectDir = resolve21(projectDirArg);
462080
+ const projectDir = resolve17(projectDirArg);
462243
462081
  const result = await getComposePrompts({
462244
462082
  projectDir,
462245
462083
  beatId: options.beat
@@ -462474,16 +462312,16 @@ async function pathExists2(p) {
462474
462312
  }
462475
462313
  }
462476
462314
  async function loadVibeProjectConfig(projectDir) {
462477
- const cfgPath = resolve21(projectDir, "vibe.project.yaml");
462315
+ const cfgPath = resolve17(projectDir, "vibe.project.yaml");
462478
462316
  if (!await pathExists2(cfgPath)) return null;
462479
- const raw2 = await readFile10(cfgPath, "utf-8");
462480
- return (0, import_yaml5.parse)(raw2);
462317
+ const raw2 = await readFile7(cfgPath, "utf-8");
462318
+ return (0, import_yaml4.parse)(raw2);
462481
462319
  }
462482
462320
  async function resolveNarrationText(value) {
462483
462321
  if (!value) return void 0;
462484
462322
  const looksLikePath = /\.[a-z]{2,4}$/i.test(value) || value.includes("/") || value.includes("\\");
462485
- if (looksLikePath && existsSync28(value)) {
462486
- return (await readFile10(value, "utf-8")).trim();
462323
+ if (looksLikePath && existsSync25(value)) {
462324
+ return (await readFile7(value, "utf-8")).trim();
462487
462325
  }
462488
462326
  return value.trim();
462489
462327
  }
@@ -462499,12 +462337,12 @@ function aspectStringFromDims(width, height) {
462499
462337
  return width > height ? "16:9" : "9:16";
462500
462338
  }
462501
462339
  async function executeSceneAdd(opts) {
462502
- const projectDir = resolve21(opts.projectDir ?? ".");
462340
+ const projectDir = resolve17(opts.projectDir ?? ".");
462503
462341
  const rootRel = opts.insertInto ?? "index.html";
462504
- const rootPath = resolve21(projectDir, rootRel);
462342
+ const rootPath = resolve17(projectDir, rootRel);
462505
462343
  const id = slugifySceneName(opts.name);
462506
462344
  const sceneRel = `compositions/scene-${id}.html`;
462507
- const scenePath = resolve21(projectDir, sceneRel);
462345
+ const scenePath = resolve17(projectDir, sceneRel);
462508
462346
  const errResult = (error) => ({
462509
462347
  success: false,
462510
462348
  id,
@@ -462521,7 +462359,7 @@ async function executeSceneAdd(opts) {
462521
462359
  if (!opts.force && await pathExists2(scenePath)) {
462522
462360
  return errResult(`Scene already exists: ${sceneRel}. Re-run with --force to overwrite.`);
462523
462361
  }
462524
- const rootHtmlBefore = await readFile10(rootPath, "utf-8");
462362
+ const rootHtmlBefore = await readFile7(rootPath, "utf-8");
462525
462363
  let dims = readRootDims(rootHtmlBefore);
462526
462364
  if (!dims) {
462527
462365
  const cfg2 = await loadVibeProjectConfig(projectDir);
@@ -462535,7 +462373,7 @@ async function executeSceneAdd(opts) {
462535
462373
  let audioAbsPath;
462536
462374
  let narrationDuration;
462537
462375
  if (opts.narrationFile && !opts.skipAudio) {
462538
- const sourceAbs = resolve21(opts.narrationFile);
462376
+ const sourceAbs = resolve17(opts.narrationFile);
462539
462377
  if (!await pathExists2(sourceAbs)) {
462540
462378
  return errResult(`Narration file not found: ${sourceAbs}`);
462541
462379
  }
@@ -462544,8 +462382,8 @@ async function executeSceneAdd(opts) {
462544
462382
  return errResult(`Unsupported narration file extension: .${ext}. Use .wav or .mp3.`);
462545
462383
  }
462546
462384
  audioRelPath = `assets/narration-${id}.${ext}`;
462547
- audioAbsPath = resolve21(projectDir, audioRelPath);
462548
- await mkdir10(dirname17(audioAbsPath), { recursive: true });
462385
+ audioAbsPath = resolve17(projectDir, audioRelPath);
462386
+ await mkdir8(dirname14(audioAbsPath), { recursive: true });
462549
462387
  await copyFile2(sourceAbs, audioAbsPath);
462550
462388
  try {
462551
462389
  narrationDuration = await getAudioDuration(audioAbsPath);
@@ -462577,9 +462415,9 @@ async function executeSceneAdd(opts) {
462577
462415
  return errResult(`${resolution.provider} TTS failed: ${tts.error ?? "unknown error"}`);
462578
462416
  }
462579
462417
  audioRelPath = `assets/narration-${id}.${resolution.audioExtension}`;
462580
- audioAbsPath = resolve21(projectDir, audioRelPath);
462581
- await mkdir10(dirname17(audioAbsPath), { recursive: true });
462582
- await writeFile10(audioAbsPath, tts.audioBuffer);
462418
+ audioAbsPath = resolve17(projectDir, audioRelPath);
462419
+ await mkdir8(dirname14(audioAbsPath), { recursive: true });
462420
+ await writeFile9(audioAbsPath, tts.audioBuffer);
462583
462421
  try {
462584
462422
  narrationDuration = await getAudioDuration(audioAbsPath);
462585
462423
  } catch {
@@ -462600,7 +462438,7 @@ async function executeSceneAdd(opts) {
462600
462438
  try {
462601
462439
  const whisper = new WhisperProvider();
462602
462440
  await whisper.initialize({ apiKey: whisperKey });
462603
- const audioBytes = await readFile10(audioAbsPath);
462441
+ const audioBytes = await readFile7(audioAbsPath);
462604
462442
  const audioBlob = new Blob([new Uint8Array(audioBytes)]);
462605
462443
  const transcript = await whisper.transcribe(audioBlob, void 0, {
462606
462444
  granularity: "word",
@@ -462608,8 +462446,8 @@ async function executeSceneAdd(opts) {
462608
462446
  });
462609
462447
  if (transcript.status === "completed" && transcript.words?.length) {
462610
462448
  transcriptRelPath = `assets/transcript-${id}.json`;
462611
- const transcriptAbs = resolve21(projectDir, transcriptRelPath);
462612
- await writeFile10(transcriptAbs, JSON.stringify(transcript.words, null, 2), "utf-8");
462449
+ const transcriptAbs = resolve17(projectDir, transcriptRelPath);
462450
+ await writeFile9(transcriptAbs, JSON.stringify(transcript.words, null, 2), "utf-8");
462613
462451
  transcriptWordCount = transcript.words.length;
462614
462452
  transcriptWords = transcript.words.map((w) => ({ text: w.text, start: w.start, end: w.end }));
462615
462453
  } else if (transcript.status === "failed") {
@@ -462645,8 +462483,8 @@ async function executeSceneAdd(opts) {
462645
462483
  }
462646
462484
  const img = imageResult.images[0];
462647
462485
  imageRelPath = `assets/scene-${id}.png`;
462648
- imageAbsPath = resolve21(projectDir, imageRelPath);
462649
- await mkdir10(dirname17(imageAbsPath), { recursive: true });
462486
+ imageAbsPath = resolve17(projectDir, imageRelPath);
462487
+ await mkdir8(dirname14(imageAbsPath), { recursive: true });
462650
462488
  let buffer;
462651
462489
  if (img.base64) {
462652
462490
  buffer = Buffer.from(img.base64, "base64");
@@ -462656,7 +462494,7 @@ async function executeSceneAdd(opts) {
462656
462494
  } else {
462657
462495
  return errResult("OpenAI returned no image data");
462658
462496
  }
462659
- await writeFile10(imageAbsPath, buffer);
462497
+ await writeFile9(imageAbsPath, buffer);
462660
462498
  } else {
462661
462499
  const googleKey = await getApiKey("GOOGLE_API_KEY", "Google");
462662
462500
  if (!googleKey) {
@@ -462670,10 +462508,10 @@ async function executeSceneAdd(opts) {
462670
462508
  return errResult(`Gemini image generation failed: ${imageResult.error ?? "unknown error"}`);
462671
462509
  }
462672
462510
  imageRelPath = `assets/scene-${id}.png`;
462673
- imageAbsPath = resolve21(projectDir, imageRelPath);
462674
- await mkdir10(dirname17(imageAbsPath), { recursive: true });
462511
+ imageAbsPath = resolve17(projectDir, imageRelPath);
462512
+ await mkdir8(dirname14(imageAbsPath), { recursive: true });
462675
462513
  const buffer = Buffer.from(imageResult.images[0].base64, "base64");
462676
- await writeFile10(imageAbsPath, buffer);
462514
+ await writeFile9(imageAbsPath, buffer);
462677
462515
  }
462678
462516
  }
462679
462517
  const cfg = await loadVibeProjectConfig(projectDir);
@@ -462706,35 +462544,35 @@ async function executeSceneAdd(opts) {
462706
462544
  audioPath: audioRelPath,
462707
462545
  transcript: transcriptWords
462708
462546
  });
462709
- await mkdir10(dirname17(scenePath), { recursive: true });
462710
- await writeFile10(scenePath, sceneHtml, "utf-8");
462547
+ await mkdir8(dirname14(scenePath), { recursive: true });
462548
+ await writeFile9(scenePath, sceneHtml, "utf-8");
462711
462549
  opts.onProgress?.("Updating root composition...");
462712
462550
  const start = nextSceneStart(rootHtmlBefore, SCENE_OVERLAP_SECONDS);
462713
462551
  const existingClipCount = (rootHtmlBefore.match(/<div\s+class="clip"/g) || []).length;
462714
462552
  const trackIndex = existingClipCount % 2 + 1;
462715
462553
  const updated = insertClipIntoRoot(rootHtmlBefore, { id, start, duration, trackIndex });
462716
- await writeFile10(rootPath, updated, "utf-8");
462717
- const transcriptAbsPath = transcriptRelPath ? resolve21(projectDir, transcriptRelPath) : void 0;
462554
+ await writeFile9(rootPath, updated, "utf-8");
462555
+ const transcriptAbsPath = transcriptRelPath ? resolve17(projectDir, transcriptRelPath) : void 0;
462718
462556
  return {
462719
462557
  success: true,
462720
462558
  id,
462721
462559
  preset: opts.preset,
462722
462560
  start,
462723
462561
  duration,
462724
- scenePath: relative7(process.cwd(), scenePath) || scenePath,
462725
- rootPath: relative7(process.cwd(), rootPath) || rootPath,
462726
- audioPath: audioAbsPath ? relative7(process.cwd(), audioAbsPath) || audioAbsPath : void 0,
462727
- imagePath: imageAbsPath ? relative7(process.cwd(), imageAbsPath) || imageAbsPath : void 0,
462728
- transcriptPath: transcriptAbsPath ? relative7(process.cwd(), transcriptAbsPath) || transcriptAbsPath : void 0,
462562
+ scenePath: relative6(process.cwd(), scenePath) || scenePath,
462563
+ rootPath: relative6(process.cwd(), rootPath) || rootPath,
462564
+ audioPath: audioAbsPath ? relative6(process.cwd(), audioAbsPath) || audioAbsPath : void 0,
462565
+ imagePath: imageAbsPath ? relative6(process.cwd(), imageAbsPath) || imageAbsPath : void 0,
462566
+ transcriptPath: transcriptAbsPath ? relative6(process.cwd(), transcriptAbsPath) || transcriptAbsPath : void 0,
462729
462567
  transcriptWordCount
462730
462568
  };
462731
462569
  }
462732
462570
  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) => {
462733
462571
  const startedAt = Date.now();
462734
- const projectDir = resolve21(options.project);
462572
+ const projectDir = resolve17(options.project);
462735
462573
  if (!await rootExists(projectDir, root2)) {
462736
462574
  exitWithError(generalError(
462737
- `Root composition not found: ${resolve21(projectDir, root2)}`,
462575
+ `Root composition not found: ${resolve17(projectDir, root2)}`,
462738
462576
  "Run `vibe scene init` first, or pass --project <dir>."
462739
462577
  ));
462740
462578
  }
@@ -462794,281 +462632,6 @@ function severityTag(severity) {
462794
462632
  if (severity === "warning") return source_default.yellow("\u26A0 warn ");
462795
462633
  return source_default.blue("\u2139 info ");
462796
462634
  }
462797
- var VALID_FPS = [24, 30, 60];
462798
- var VALID_QUALITIES = ["draft", "standard", "high"];
462799
- var VALID_FORMATS = ["mp4", "webm", "mov"];
462800
- function validateFps(value) {
462801
- const n = parseInt(value, 10);
462802
- if (!VALID_FPS.includes(n)) {
462803
- exitWithError(usageError(`Invalid --fps: ${value}`, `Valid: ${VALID_FPS.join(", ")}`));
462804
- }
462805
- return n;
462806
- }
462807
- function validateQuality(value) {
462808
- if (!VALID_QUALITIES.includes(value)) {
462809
- exitWithError(usageError(`Invalid --quality: ${value}`, `Valid: ${VALID_QUALITIES.join(", ")}`));
462810
- }
462811
- return value;
462812
- }
462813
- function validateFormat(value) {
462814
- if (!VALID_FORMATS.includes(value)) {
462815
- exitWithError(usageError(`Invalid --format: ${value}`, `Valid: ${VALID_FORMATS.join(", ")}`));
462816
- }
462817
- return value;
462818
- }
462819
- function validateWorkers(value) {
462820
- const n = parseInt(value, 10);
462821
- if (!Number.isFinite(n) || n < 1 || n > 16) {
462822
- exitWithError(usageError(`Invalid --workers: ${value}`, "Must be an integer between 1 and 16"));
462823
- }
462824
- return n;
462825
- }
462826
- sceneCommand.command("render", { hidden: true }).description("Render a scene project to MP4/WebM/MOV via the Hyperframes producer (requires Chrome) [legacy \u2014 prefer `vibe render`]").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) => {
462827
- const startedAt = Date.now();
462828
- const fps = validateFps(options.fps);
462829
- const quality = validateQuality(options.quality);
462830
- const format4 = validateFormat(options.format);
462831
- const workers = validateWorkers(options.workers);
462832
- const projectDir = resolve21(options.project);
462833
- if (options.dryRun) {
462834
- outputSuccess({
462835
- command: "scene render",
462836
- startedAt,
462837
- dryRun: true,
462838
- data: {
462839
- params: {
462840
- projectDir,
462841
- root: root2,
462842
- output: options.out,
462843
- fps,
462844
- quality,
462845
- format: format4,
462846
- workers
462847
- }
462848
- }
462849
- });
462850
- return;
462851
- }
462852
- const spinner2 = isJsonMode() ? null : ora("Rendering scene project...").start();
462853
- const result = await executeSceneRender({
462854
- projectDir,
462855
- root: root2,
462856
- output: options.out,
462857
- fps,
462858
- quality,
462859
- format: format4,
462860
- workers,
462861
- onProgress: (pct, stage) => {
462862
- if (spinner2) spinner2.text = `Rendering [${Math.round(pct * 100)}%] ${stage}`;
462863
- }
462864
- });
462865
- if (!result.success) {
462866
- spinner2?.fail("Render failed");
462867
- if (isJsonMode()) {
462868
- outputSuccess({
462869
- command: "scene render",
462870
- startedAt,
462871
- data: { ...result }
462872
- });
462873
- process.exitCode = 1;
462874
- return;
462875
- }
462876
- exitWithError(generalError(result.error ?? "Render failed"));
462877
- }
462878
- if (isJsonMode()) {
462879
- outputSuccess({
462880
- command: "scene render",
462881
- startedAt,
462882
- data: { ...result }
462883
- });
462884
- return;
462885
- }
462886
- spinner2?.succeed(source_default.green(`Render complete: ${result.outputPath}`));
462887
- console.log();
462888
- console.log(source_default.bold.cyan("Render"));
462889
- console.log(source_default.dim("\u2500".repeat(60)));
462890
- console.log(` output ${source_default.bold(result.outputPath)}`);
462891
- console.log(` duration ${((result.durationMs ?? 0) / 1e3).toFixed(1)}s`);
462892
- console.log(` frames ${result.framesRendered ?? "?"}${result.totalFrames ? ` / ${result.totalFrames}` : ""}`);
462893
- console.log(` config ${result.fps}fps \xB7 ${result.quality} \xB7 ${result.format}`);
462894
- if (result.audioCount && result.audioCount > 0) {
462895
- const muxStatus = result.audioMuxApplied ? source_default.green(`\u2713 ${result.audioCount} track${result.audioCount === 1 ? "" : "s"} muxed`) : source_default.yellow(`\u26A0 ${result.audioCount} track${result.audioCount === 1 ? "" : "s"} skipped`);
462896
- console.log(` audio ${muxStatus}`);
462897
- if (result.audioMuxWarning) {
462898
- console.log(source_default.dim(` ${result.audioMuxWarning}`));
462899
- }
462900
- }
462901
- });
462902
- sceneCommand.command("build", { hidden: true }).description("One-shot: read STORYBOARD.md cues, dispatch TTS + image-gen per beat, compose, render to MP4 [legacy \u2014 prefer `vibe build`]").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) => {
462903
- const startedAt = Date.now();
462904
- const projectDir = resolve21(projectDirArg);
462905
- if (options.dryRun) {
462906
- outputSuccess({
462907
- command: "scene build",
462908
- startedAt,
462909
- dryRun: true,
462910
- data: {
462911
- params: {
462912
- projectDir,
462913
- mode: options.mode,
462914
- effort: options.effort,
462915
- composer: options.composer,
462916
- skipNarration: options.skipNarration ?? false,
462917
- skipBackdrop: options.skipBackdrop ?? false,
462918
- skipRender: options.skipRender ?? false,
462919
- ttsProvider: options.tts,
462920
- voice: options.voice,
462921
- imageProvider: options.imageProvider,
462922
- imageQuality: options.quality,
462923
- imageSize: options.imageSize,
462924
- force: options.force ?? false
462925
- }
462926
- }
462927
- });
462928
- return;
462929
- }
462930
- const validEfforts = ["low", "medium", "high"];
462931
- if (!validEfforts.includes(options.effort)) {
462932
- exitWithError(usageError(`Invalid --effort: ${options.effort}`, `Must be one of: ${validEfforts.join(", ")}`));
462933
- }
462934
- const validComposers = ["claude", "openai", "gemini"];
462935
- if (options.composer !== void 0 && !validComposers.includes(options.composer)) {
462936
- exitWithError(usageError(`Invalid --composer: ${options.composer}`, `Must be one of: ${validComposers.join(", ")}`));
462937
- }
462938
- const validModes = ["agent", "batch", "auto"];
462939
- if (options.mode !== void 0 && !validModes.includes(options.mode)) {
462940
- exitWithError(usageError(`Invalid --mode: ${options.mode}`, `Must be one of: ${validModes.join(", ")}`));
462941
- }
462942
- const spinner2 = isJsonMode() ? null : ora("Reading STORYBOARD.md...").start();
462943
- const result = await executeSceneBuild({
462944
- projectDir,
462945
- mode: options.mode,
462946
- effort: options.effort,
462947
- composer: options.composer,
462948
- skipNarration: options.skipNarration,
462949
- skipBackdrop: options.skipBackdrop,
462950
- skipRender: options.skipRender,
462951
- ttsProvider: options.tts,
462952
- voice: options.voice,
462953
- imageProvider: options.imageProvider,
462954
- imageQuality: options.quality,
462955
- imageSize: options.imageSize,
462956
- force: options.force,
462957
- onProgress: (e) => {
462958
- if (!spinner2) return;
462959
- if (e.type === "phase-start") {
462960
- spinner2.text = `Phase: ${e.phase}...`;
462961
- } else if (e.type === "narration-generated") {
462962
- spinner2.text = `Narration ${e.beatId} \u2192 ${e.path} (${e.provider})`;
462963
- } else if (e.type === "backdrop-generated") {
462964
- spinner2.text = `Backdrop ${e.beatId} \u2192 ${e.path} (${e.provider})`;
462965
- } else if (e.type === "beat-fresh") {
462966
- spinner2.text = `Composed beat ${e.beatId} ($${(e.costUsd ?? 0).toFixed(3)} \xB7 ${e.latencyMs ?? 0}ms)`;
462967
- } else if (e.type === "beat-cached") {
462968
- spinner2.text = `Composed beat ${e.beatId} (cached)`;
462969
- } else if (e.type === "render-start") {
462970
- spinner2.text = "Rendering...";
462971
- } else if (e.type === "render-done") {
462972
- spinner2.text = `Rendered: ${e.outputPath}`;
462973
- }
462974
- }
462975
- });
462976
- if (!result.success) {
462977
- spinner2?.fail(`Build failed: ${result.error}`);
462978
- if (isJsonMode()) {
462979
- outputSuccess({
462980
- command: "scene build",
462981
- startedAt,
462982
- data: { ...result }
462983
- });
462984
- process.exitCode = 1;
462985
- return;
462986
- }
462987
- exitWithError(generalError(result.error ?? "Build failed"));
462988
- }
462989
- if (isJsonMode()) {
462990
- outputSuccess({
462991
- command: "scene build",
462992
- startedAt,
462993
- data: { ...result }
462994
- });
462995
- return;
462996
- }
462997
- if (result.phase === "needs-author") {
462998
- spinner2?.info(source_default.cyan("Agent mode \u2014 host agent must author scene HTML before rendering"));
462999
- console.log();
463000
- console.log(source_default.bold.cyan("Beats requiring authorship"));
463001
- console.log(source_default.dim("\u2500".repeat(60)));
463002
- const plan = result.composePrompts;
463003
- if (plan) {
463004
- for (const b of plan.beats) {
463005
- const status = b.exists ? source_default.dim("(exists)") : source_default.green("(needs author)");
463006
- const dur = b.duration !== void 0 ? source_default.dim(` ${b.duration}s`) : "";
463007
- console.log(` ${source_default.bold(b.id)}${dur} \u2192 ${b.outputPath} ${status}`);
463008
- }
463009
- console.log();
463010
- console.log(source_default.bold.cyan("Instructions"));
463011
- console.log(source_default.dim("\u2500".repeat(60)));
463012
- for (const line of plan.instructions) console.log(` ${line}`);
463013
- if (plan.warnings.length > 0) {
463014
- console.log();
463015
- for (const w of plan.warnings) console.log(source_default.yellow(` \u26A0 ${w}`));
463016
- }
463017
- }
463018
- console.log();
463019
- console.log(source_default.dim("Once you've authored each beat's HTML, re-run `vibe build` to lint + render."));
463020
- console.log(source_default.dim("Or pass `--mode batch` to use the internal LLM compose path instead."));
463021
- return;
463022
- }
463023
- spinner2?.succeed(source_default.green(
463024
- result.outputPath ? `Build complete: ${result.outputPath}` : "Build complete (compose only \u2014 render skipped)"
463025
- ));
463026
- console.log();
463027
- console.log(source_default.bold.cyan("Beats"));
463028
- console.log(source_default.dim("\u2500".repeat(60)));
463029
- for (const b of result.beats) {
463030
- const narration = formatPrimitiveStatus(b.narrationStatus, b.narrationPath);
463031
- const backdrop = formatPrimitiveStatus(b.backdropStatus, b.backdropPath);
463032
- console.log(` ${source_default.bold(b.beatId.padEnd(12))} narration: ${narration} backdrop: ${backdrop}`);
463033
- if (b.narrationError) console.log(source_default.red(` ! narration: ${b.narrationError}`));
463034
- if (b.backdropError) console.log(source_default.red(` ! backdrop: ${b.backdropError}`));
463035
- }
463036
- if (result.composeData) {
463037
- console.log();
463038
- console.log(source_default.bold.cyan("Compose"));
463039
- console.log(source_default.dim("\u2500".repeat(60)));
463040
- console.log(` beats ${result.composeData.beats}`);
463041
- console.log(` cache ${result.composeData.cacheHits} hit / ${result.composeData.beats - result.composeData.cacheHits} fresh`);
463042
- console.log(` cost $${result.composeData.totalCostUsd.toFixed(4)}`);
463043
- }
463044
- if (result.outputPath) {
463045
- console.log();
463046
- console.log(source_default.bold.cyan("Render"));
463047
- console.log(source_default.dim("\u2500".repeat(60)));
463048
- console.log(` output ${source_default.bold(result.outputPath)}`);
463049
- if (result.renderResult?.audioCount && result.renderResult.audioCount > 0) {
463050
- console.log(` audio ${result.renderResult.audioCount} track${result.renderResult.audioCount === 1 ? "" : "s"} muxed`);
463051
- }
463052
- }
463053
- console.log();
463054
- console.log(source_default.dim(`Total: ${(result.totalLatencyMs / 1e3).toFixed(1)}s`));
463055
- });
463056
- function formatPrimitiveStatus(status, path14) {
463057
- switch (status) {
463058
- case "generated":
463059
- return source_default.green(`\u2713 ${path14}`);
463060
- case "cached":
463061
- return source_default.dim(`\u25C7 ${path14} (cached)`);
463062
- case "skipped":
463063
- return source_default.dim("\xB7 skipped");
463064
- case "no-cue":
463065
- return source_default.dim("\xB7 no cue");
463066
- case "failed":
463067
- return source_default.red("\u2717 failed");
463068
- default:
463069
- return status;
463070
- }
463071
- }
463072
462635
 
463073
462636
  // ../cli/src/tools/manifest/scene.ts
463074
462637
  init_scene_lint();
@@ -463663,7 +463226,7 @@ init_ai_edit();
463663
463226
  init_api_key();
463664
463227
  init_exec_safe();
463665
463228
  init_remotion();
463666
- import { resolve as resolve30, dirname as dirname23, basename as basename10 } from "node:path";
463229
+ import { resolve as resolve30, dirname as dirname23, basename as basename9 } from "node:path";
463667
463230
  import { writeFile as writeFile18, mkdir as mkdir16, rm as rm4 } from "node:fs/promises";
463668
463231
  import { existsSync as existsSync41 } from "node:fs";
463669
463232
  import { tmpdir as tmpdir4 } from "node:os";
@@ -463878,7 +463441,7 @@ async function executeAnimatedCaption(options) {
463878
463441
  width,
463879
463442
  height,
463880
463443
  fps: videoFps,
463881
- videoFileName: basename10(videoPath)
463444
+ videoFileName: basename9(videoPath)
463882
463445
  });
463883
463446
  const durationInFrames = Math.ceil(duration * videoFps);
463884
463447
  const renderResult = await renderWithEmbeddedVideo({
@@ -463889,7 +463452,7 @@ async function executeAnimatedCaption(options) {
463889
463452
  fps: videoFps,
463890
463453
  durationInFrames,
463891
463454
  videoPath,
463892
- videoFileName: basename10(videoPath),
463455
+ videoFileName: basename9(videoPath),
463893
463456
  outputPath: absOutputPath
463894
463457
  });
463895
463458
  if (!renderResult.success) {
@@ -464811,7 +464374,7 @@ init_ai_script_pipeline();
464811
464374
 
464812
464375
  // ../cli/src/commands/ai-highlights.ts
464813
464376
  import { readFile as readFile27, writeFile as writeFile35, mkdir as mkdir21 } from "node:fs/promises";
464814
- import { resolve as resolve51, dirname as dirname28, basename as basename13, extname as extname11 } from "node:path";
464377
+ import { resolve as resolve51, dirname as dirname28, basename as basename12, extname as extname11 } from "node:path";
464815
464378
  import { existsSync as existsSync49 } from "node:fs";
464816
464379
  init_dist();
464817
464380
  init_engine();
@@ -465005,7 +464568,7 @@ Analyze both what is SHOWN (visual cues, actions, expressions) and what is SAID
465005
464568
  if (options.project) {
465006
464569
  const project = new Project("Highlight Reel");
465007
464570
  const source3 = project.addSource({
465008
- name: basename13(absPath),
464571
+ name: basename12(absPath),
465009
464572
  url: absPath,
465010
464573
  type: isVideo ? "video" : "audio",
465011
464574
  duration: sourceDuration
@@ -465183,7 +464746,7 @@ Analyze both VISUALS (expressions, actions, scene changes) and AUDIO (speech, re
465183
464746
  };
465184
464747
  for (let i = 0; i < selectedHighlights.length; i++) {
465185
464748
  const h = selectedHighlights[i];
465186
- const baseName = basename13(absPath, extname11(absPath));
464749
+ const baseName = basename12(absPath, extname11(absPath));
465187
464750
  const outputPath = resolve51(outputDir, `${baseName}-short-${i + 1}.mp4`);
465188
464751
  const { stdout: probeOut } = await execSafe("ffprobe", [
465189
464752
  "-v",
@@ -466361,7 +465924,7 @@ init_exec_safe();
466361
465924
  init_output();
466362
465925
  init_validate();
466363
465926
  import { readFile as readFile31, access as access5, stat as stat3 } from "node:fs/promises";
466364
- import { resolve as resolve59, basename as basename16 } from "node:path";
465927
+ import { resolve as resolve59, basename as basename15 } from "node:path";
466365
465928
  import { spawn as spawn9 } from "node:child_process";
466366
465929
  async function resolveProjectPath(inputPath) {
466367
465930
  const filePath = resolve59(process.cwd(), inputPath);
@@ -466576,7 +466139,7 @@ Run 'vibe schema export' for structured parameter info.`).action(async (projectP
466576
466139
  }
466577
466140
  const outputPath = options.output ? resolve59(process.cwd(), options.output) : resolve59(
466578
466141
  process.cwd(),
466579
- `${basename16(projectPath, ".vibe.json")}.${options.format}`
466142
+ `${basename15(projectPath, ".vibe.json")}.${options.format}`
466580
466143
  );
466581
466144
  const basePresetSettings = getPresetSettings(options.preset, summary.aspectRatio);
466582
466145
  const presetSettings = applyCustomOverrides(basePresetSettings, customOverrides);
@@ -467165,7 +466728,7 @@ function getPresetSettings(preset, aspectRatio) {
467165
466728
  async function runHyperframesExport(projectPath, options, spinner2, startedAt) {
467166
466729
  spinner2.text = "Loading project...";
467167
466730
  const { readFile: readFile34 } = await import("node:fs/promises");
467168
- const { resolve: resolve64, basename: basename18 } = await import("node:path");
466731
+ const { resolve: resolve64, basename: basename17 } = await import("node:path");
467169
466732
  const { Project: Project2 } = await Promise.resolve().then(() => (init_engine(), engine_exports));
467170
466733
  const { createHyperframesBackend: createHyperframesBackend2 } = await Promise.resolve().then(() => (init_hyperframes(), hyperframes_exports));
467171
466734
  const { exitWithError: exitWithError2, generalError: generalError2, outputSuccess: outputSuccess2 } = await Promise.resolve().then(() => (init_output(), output_exports));
@@ -467174,7 +466737,7 @@ async function runHyperframesExport(projectPath, options, spinner2, startedAt) {
467174
466737
  const content = await readFile34(filePath, "utf-8");
467175
466738
  const project = Project2.fromJSON(JSON.parse(content));
467176
466739
  const state = project.getState();
467177
- const outputPath = options.output ? resolve64(process.cwd(), options.output) : resolve64(process.cwd(), `${basename18(projectPath, ".vibe.json")}.${options.format ?? "mp4"}`);
466740
+ const outputPath = options.output ? resolve64(process.cwd(), options.output) : resolve64(process.cwd(), `${basename17(projectPath, ".vibe.json")}.${options.format ?? "mp4"}`);
467178
466741
  const quality = ["draft", "standard", "high"].includes(options.preset ?? "") ? options.preset : "standard";
467179
466742
  const backend = createHyperframesBackend2();
467180
466743
  spinner2.text = "Rendering with Hyperframes...";
@@ -467239,7 +466802,7 @@ var exportTools = [exportVideoTool];
467239
466802
 
467240
466803
  // ../cli/src/tools/manifest/agent-only.ts
467241
466804
  import { readFile as readFile32, writeFile as writeFile42, readdir as readdir5, stat as stat4, access as access6, unlink as unlink7 } from "node:fs/promises";
467242
- import { resolve as resolve61, join as join33, basename as basename17, extname as extname12 } from "node:path";
466805
+ import { resolve as resolve61, join as join33, basename as basename16, extname as extname12 } from "node:path";
467243
466806
  import { z as z11 } from "zod";
467244
466807
  init_engine();
467245
466808
  init_exec_safe();
@@ -467437,7 +467000,7 @@ var batchImportTool = defineTool({
467437
467000
  mediaFiles.sort();
467438
467001
  const addedSources = [];
467439
467002
  for (const mediaFile of mediaFiles) {
467440
- const mediaName = basename17(mediaFile);
467003
+ const mediaName = basename16(mediaFile);
467441
467004
  const mediaType = detectMediaType(mediaFile);
467442
467005
  let duration = imageDuration;
467443
467006
  if (mediaType !== "image") {