@vibeframe/cli 0.98.0 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CONTEXT.md +2 -1
  2. package/dist/index.js +197 -16
  3. package/package.json +3 -3
package/CONTEXT.md CHANGED
@@ -132,7 +132,7 @@ legacy vibe.project.yaml → configured/env default → VibeFrame default
132
132
  |-------|----------|
133
133
  | `generate` | Always `--dry-run` first. Costs money. `-p` selects provider; default routes via key availability. |
134
134
  | `edit` | FFmpeg-only leaves (silence-cut, fade, noise-reduce, text-overlay, interpolate) are free. caption/grade/reframe/image need API keys. |
135
- | `inspect` | `inspect project` and `inspect render --cheap` are free/local. Gemini media/review calls are low cost. |
135
+ | `inspect` | `inspect project` and `inspect render --cheap` are local. Use `inspect render --ai` for final Gemini critique; Gemini media/review calls are low cost. |
136
136
  | `audio` | `transcribe` low cost (Whisper). `dub` is full pipeline (medium-high). `duck` is free. |
137
137
  | `remix` | **Confirm with user.** Multi-step, high cost ($5–50+). Always `--dry-run`. |
138
138
  | `detect` | Free, FFmpeg only. No keys. |
@@ -163,6 +163,7 @@ vibe build my-video --max-cost 5 --json
163
163
  vibe inspect project my-video --json
164
164
  vibe render my-video -o renders/final.mp4 --json
165
165
  vibe inspect render my-video --cheap --json
166
+ vibe inspect render my-video --ai --json
166
167
  vibe scene repair --project my-video --json
167
168
  ```
168
169
 
package/dist/index.js CHANGED
@@ -41385,16 +41385,79 @@ function parseSilencedetectOutput(output) {
41385
41385
  }
41386
41386
  return ranges;
41387
41387
  }
41388
+ async function previewInspectRender(opts) {
41389
+ const projectDir = resolve38(opts.projectDir);
41390
+ const videoPath = await resolveRenderVideoPath(projectDir, opts.videoPath);
41391
+ const writeReport = opts.writeReport !== false;
41392
+ const reportPath = writeReport ? opts.outputPath ? resolve38(process.cwd(), opts.outputPath) : defaultReviewReportPath(projectDir) : void 0;
41393
+ const storyboardPath2 = resolveStoryboardPath(projectDir);
41394
+ return {
41395
+ schemaVersion: "1",
41396
+ kind: "render",
41397
+ project: projectDir,
41398
+ videoPath,
41399
+ reportPath,
41400
+ params: {
41401
+ projectDir,
41402
+ videoPath: opts.videoPath,
41403
+ outputPath: opts.outputPath,
41404
+ writeReport,
41405
+ cheap: true,
41406
+ ai: opts.ai === true,
41407
+ model: opts.model ?? DEFAULT_AI_MODEL
41408
+ },
41409
+ checks: {
41410
+ renderFound: videoPath !== null,
41411
+ storyboardPath: storyboardPath2
41412
+ }
41413
+ };
41414
+ }
41415
+ function aiReviewSeverity(score) {
41416
+ if (score <= 4) return "error";
41417
+ if (score <= 6) return "warning";
41418
+ return "info";
41419
+ }
41420
+ function mapAiReviewFeedbackToIssues(feedback, videoPath) {
41421
+ const issues = [];
41422
+ for (const [key, label] of Object.entries(AI_CATEGORY_LABELS)) {
41423
+ const category = feedback.categories[key];
41424
+ if (!category || category.issues.length === 0) continue;
41425
+ for (const issue of category.issues) {
41426
+ issues.push({
41427
+ severity: aiReviewSeverity(category.score),
41428
+ code: `AI_REVIEW_${toSnakeCase(String(key))}`,
41429
+ message: `${label}: ${issue}`,
41430
+ file: videoPath ? displayPath(videoPath) : void 0,
41431
+ suggestedFix: category.fixable ? "Adjust the relevant storyboard or scene composition, then rerender." : "Review this finding manually before rerendering."
41432
+ });
41433
+ }
41434
+ }
41435
+ return issues;
41436
+ }
41437
+ function scoreRenderReview(issues, aiOverallScore) {
41438
+ const localScore = scoreIssues(issues);
41439
+ if (aiOverallScore === void 0) return localScore;
41440
+ const aiScore = Math.max(0, Math.min(100, Math.round(aiOverallScore * 10)));
41441
+ return Math.round((localScore + aiScore) / 2);
41442
+ }
41388
41443
  async function inspectRender(opts) {
41389
41444
  const projectDir = resolve38(opts.projectDir);
41390
41445
  const issues = [];
41391
41446
  const retryWith = [];
41447
+ const model = opts.model ?? DEFAULT_AI_MODEL;
41392
41448
  const videoPath = await resolveRenderVideoPath(projectDir, opts.videoPath);
41393
41449
  const checks = {
41394
41450
  renderFound: videoPath !== null,
41395
41451
  blackFrames: [],
41396
41452
  silences: []
41397
41453
  };
41454
+ if (opts.ai) {
41455
+ checks.ai = {
41456
+ enabled: true,
41457
+ model,
41458
+ success: false
41459
+ };
41460
+ }
41398
41461
  if (!videoPath) {
41399
41462
  issues.push({
41400
41463
  severity: "error",
@@ -41403,13 +41466,16 @@ async function inspectRender(opts) {
41403
41466
  suggestedFix: "Run `vibe build --stage render --json` or `vibe render --json`."
41404
41467
  });
41405
41468
  retryWith.push(`vibe build ${projectDir} --stage render --json`, `vibe render ${projectDir} --json`);
41469
+ if (checks.ai) {
41470
+ checks.ai.error = "Skipped AI review because no rendered video was found.";
41471
+ }
41406
41472
  return maybeWriteRenderReport(projectDir, opts, {
41407
41473
  schemaVersion: "1",
41408
41474
  kind: "render",
41409
41475
  project: projectDir,
41410
41476
  videoPath: null,
41411
41477
  status: "fail",
41412
- score: scoreIssues(issues),
41478
+ score: scoreRenderReview(issues),
41413
41479
  issues,
41414
41480
  checks,
41415
41481
  retryWith: uniqueRetryWith(retryWith)
@@ -41547,6 +41613,40 @@ async function inspectRender(opts) {
41547
41613
  suggestedFix: "Install FFmpeg for full cheap render inspection."
41548
41614
  });
41549
41615
  }
41616
+ let aiOverallScore;
41617
+ if (opts.ai && checks.ai) {
41618
+ if (fileStat.size === 0) {
41619
+ checks.ai.error = "Skipped AI review because the rendered video file is empty.";
41620
+ } else {
41621
+ const aiResult = await runAiRenderReview(projectDir, videoPath, model);
41622
+ if (aiResult.success && aiResult.feedback) {
41623
+ checks.ai.success = true;
41624
+ checks.ai.overallScore = aiResult.feedback.overallScore;
41625
+ checks.ai.categories = aiResult.feedback.categories;
41626
+ checks.ai.recommendations = aiResult.feedback.recommendations;
41627
+ aiOverallScore = aiResult.feedback.overallScore;
41628
+ const aiIssues = mapAiReviewFeedbackToIssues(aiResult.feedback, videoPath);
41629
+ issues.push(...aiIssues);
41630
+ if (aiIssues.length > 0) {
41631
+ retryWith.push(
41632
+ 'codex "fix issues from review-report.json"',
41633
+ `vibe render ${projectDir} --json`,
41634
+ `vibe inspect render ${projectDir} --ai --json`
41635
+ );
41636
+ }
41637
+ } else {
41638
+ const message = aiResult.error ?? "Gemini video review failed";
41639
+ checks.ai.error = message;
41640
+ issues.push({
41641
+ severity: "error",
41642
+ code: "AI_REVIEW_FAILED",
41643
+ message: `AI render review failed: ${message}`,
41644
+ file: displayPath(videoPath),
41645
+ suggestedFix: "Set GOOGLE_API_KEY or retry the AI review later."
41646
+ });
41647
+ }
41648
+ }
41649
+ }
41550
41650
  const status = statusFromIssues(issues);
41551
41651
  const result = {
41552
41652
  schemaVersion: "1",
@@ -41554,7 +41654,7 @@ async function inspectRender(opts) {
41554
41654
  project: projectDir,
41555
41655
  videoPath,
41556
41656
  status,
41557
- score: scoreIssues(issues),
41657
+ score: scoreRenderReview(issues, aiOverallScore),
41558
41658
  issues,
41559
41659
  checks,
41560
41660
  retryWith: uniqueRetryWith(retryWith)
@@ -41603,6 +41703,19 @@ async function resolveRenderVideoPath(projectDir, explicit) {
41603
41703
  candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
41604
41704
  return candidates[0]?.path ?? null;
41605
41705
  }
41706
+ async function runAiRenderReview(projectDir, videoPath, model) {
41707
+ return executeReview({
41708
+ videoPath,
41709
+ storyboardPath: resolveStoryboardPath(projectDir) ?? void 0,
41710
+ autoApply: false,
41711
+ verify: false,
41712
+ model
41713
+ });
41714
+ }
41715
+ function resolveStoryboardPath(projectDir) {
41716
+ const storyboardPath2 = join14(projectDir, "STORYBOARD.md");
41717
+ return existsSync30(storyboardPath2) ? storyboardPath2 : null;
41718
+ }
41606
41719
  async function expectedDurationFromBuildReport(projectDir) {
41607
41720
  const reportPath = join14(projectDir, "build-report.json");
41608
41721
  if (!existsSync30(reportPath)) return void 0;
@@ -41664,14 +41777,26 @@ function aspectRatio(aspect) {
41664
41777
  function displayPath(path2) {
41665
41778
  return relative8(process.cwd(), path2) || basename11(path2);
41666
41779
  }
41667
- var VIDEO_EXTENSIONS;
41780
+ function toSnakeCase(value) {
41781
+ return value.replace(/[A-Z]/g, (match) => `_${match}`).toUpperCase();
41782
+ }
41783
+ var VIDEO_EXTENSIONS, DEFAULT_AI_MODEL, AI_CATEGORY_LABELS;
41668
41784
  var init_render_inspect = __esm({
41669
41785
  "src/commands/_shared/render-inspect.ts"() {
41670
41786
  "use strict";
41787
+ init_ai_review();
41671
41788
  init_exec_safe();
41672
41789
  init_project_config();
41673
41790
  init_review_report();
41674
41791
  VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".m4v"]);
41792
+ DEFAULT_AI_MODEL = "flash";
41793
+ AI_CATEGORY_LABELS = {
41794
+ pacing: "Pacing",
41795
+ color: "Color",
41796
+ textReadability: "Text readability",
41797
+ audioVisualSync: "Audio-visual sync",
41798
+ composition: "Composition"
41799
+ };
41675
41800
  }
41676
41801
  });
41677
41802
 
@@ -62587,21 +62712,44 @@ var init_inspect = __esm({
62587
62712
  inspectRenderTool = defineTool({
62588
62713
  name: "inspect_render",
62589
62714
  category: "analyze",
62590
- cost: "free",
62591
- description: "Cheap local render inspection: finds the rendered video, probes duration/dimensions/audio, scans black frames and long silence, and writes review-report.json by default.",
62715
+ cost: "low",
62716
+ description: "Render inspection: runs cheap local video checks by default, optionally adds Gemini AI review with ai: true, and writes review-report.json by default.",
62592
62717
  schema: external_exports.object({
62593
62718
  projectDir: external_exports.string().optional().describe("Project directory. Defaults to the surface's cwd."),
62594
62719
  videoPath: external_exports.string().optional().describe("Rendered video path. Defaults to build-report outputPath or latest renders/* video."),
62595
62720
  outputPath: external_exports.string().optional().describe("Optional review report path. Defaults to <project>/review-report.json."),
62596
- report: external_exports.boolean().optional().describe("Write review-report.json. Default true.")
62721
+ report: external_exports.boolean().optional().describe("Write review-report.json. Default true."),
62722
+ ai: external_exports.boolean().optional().describe("Also run Gemini video review and merge findings into review-report.json. Default false."),
62723
+ model: external_exports.enum(["flash", "flash-2.5", "pro"]).optional().describe("Gemini model variant for ai review. Default flash."),
62724
+ dryRun: external_exports.boolean().optional().describe("Preview resolved inputs without probing video or calling Gemini.")
62597
62725
  }),
62598
62726
  async execute(args, ctx) {
62599
62727
  const projectDir = args.projectDir ? resolve68(ctx.workingDirectory, args.projectDir) : ctx.workingDirectory;
62728
+ if (args.dryRun) {
62729
+ const result2 = await previewInspectRender({
62730
+ projectDir,
62731
+ videoPath: args.videoPath ? resolve68(ctx.workingDirectory, args.videoPath) : void 0,
62732
+ outputPath: args.outputPath ? resolve68(ctx.workingDirectory, args.outputPath) : void 0,
62733
+ writeReport: args.report !== false,
62734
+ ai: args.ai === true,
62735
+ model: args.model
62736
+ });
62737
+ return {
62738
+ success: true,
62739
+ data: result2,
62740
+ humanLines: [
62741
+ `Render inspection dry-run \u2014 ${result2.videoPath ? "render found" : "render missing"}`,
62742
+ ...result2.reportPath ? [`report: ${result2.reportPath}`] : []
62743
+ ]
62744
+ };
62745
+ }
62600
62746
  const result = await inspectRender({
62601
62747
  projectDir,
62602
62748
  videoPath: args.videoPath ? resolve68(ctx.workingDirectory, args.videoPath) : void 0,
62603
62749
  outputPath: args.outputPath ? resolve68(ctx.workingDirectory, args.outputPath) : void 0,
62604
- writeReport: args.report !== false
62750
+ writeReport: args.report !== false,
62751
+ ai: args.ai === true,
62752
+ model: args.model
62605
62753
  });
62606
62754
  return {
62607
62755
  success: result.status !== "fail",
@@ -66074,7 +66222,7 @@ init_source();
66074
66222
  // package.json
66075
66223
  var package_default = {
66076
66224
  name: "@vibeframe/cli",
66077
- version: "0.98.0",
66225
+ version: "0.99.0",
66078
66226
  description: "VibeFrame CLI - natural language video editing from the terminal",
66079
66227
  private: false,
66080
66228
  license: "MIT",
@@ -67264,12 +67412,14 @@ init_scene_inspect();
67264
67412
  init_render_inspect();
67265
67413
  import { readFile as readFile26, writeFile as writeFile32 } from "node:fs/promises";
67266
67414
  import { resolve as resolve39 } from "node:path";
67415
+ var VALID_RENDER_AI_MODELS = ["flash", "flash-2.5", "pro"];
67267
67416
  var inspectCommand = new Command("inspect").description("Inspect media using AI (images, videos, YouTube URLs)").addHelpText(
67268
67417
  "after",
67269
67418
  `
67270
67419
  Examples:
67271
67420
  $ vibe inspect project my-video --json
67272
67421
  $ vibe inspect render my-video --cheap --json
67422
+ $ vibe inspect render my-video --ai --json
67273
67423
  $ vibe inspect media image.png "Describe this image"
67274
67424
  $ vibe inspect media video.mp4 "Summarize this video"
67275
67425
  $ vibe inspect media "https://youtube.com/watch?v=..." "Key takeaways"
@@ -67308,14 +67458,36 @@ inspectCommand.command("project").description("Inspect project completeness, sto
67308
67458
  }
67309
67459
  });
67310
67460
  applyTier(inspectCommand.commands[inspectCommand.commands.length - 1], "free");
67311
- inspectCommand.command("render").description("Inspect a rendered project video with local cheap checks").argument("[project-dir]", "VibeFrame project directory", ".").option("--cheap", "Run local checks only (default; no AI/API calls)").option("--video <path>", "Rendered video path. Defaults to build-report outputPath or latest renders/* video.").option("-o, --output <path>", "Write review report to this path (default: <project>/review-report.json)").option("--no-report", "Do not write review-report.json").action(async (projectDirArg, options) => {
67461
+ inspectCommand.command("render").description("Inspect a rendered project video with local checks and optional Gemini review").argument("[project-dir]", "VibeFrame project directory", ".").option("--cheap", "Run local checks only (default; no AI/API calls)").option("--ai", "Also run Gemini video review and merge findings into review-report.json").option("-m, --model <model>", "Gemini model for --ai: flash (default), flash-2.5, pro", "flash").option("--video <path>", "Rendered video path. Defaults to build-report outputPath or latest renders/* video.").option("-o, --output <path>", "Write review report to this path (default: <project>/review-report.json)").option("--no-report", "Do not write review-report.json").option("--dry-run", "Preview parameters without probing video or calling Gemini").action(async (projectDirArg, options) => {
67312
67462
  const startedAt = Date.now();
67313
67463
  try {
67464
+ const model = parseRenderAiModel(options.model);
67465
+ const projectDir = resolve39(projectDirArg);
67466
+ if (options.dryRun) {
67467
+ const result2 = await previewInspectRender({
67468
+ projectDir,
67469
+ videoPath: options.video,
67470
+ outputPath: options.output,
67471
+ writeReport: options.report !== false,
67472
+ ai: options.ai === true,
67473
+ model
67474
+ });
67475
+ outputSuccess({
67476
+ command: "inspect render",
67477
+ startedAt,
67478
+ dryRun: true,
67479
+ costUsd: options.ai ? 0.1 : 0,
67480
+ data: { ...result2 }
67481
+ });
67482
+ return;
67483
+ }
67314
67484
  const result = await inspectRender({
67315
- projectDir: resolve39(projectDirArg),
67485
+ projectDir,
67316
67486
  videoPath: options.video,
67317
67487
  outputPath: options.output,
67318
- writeReport: options.report !== false
67488
+ writeReport: options.report !== false,
67489
+ ai: options.ai === true,
67490
+ model
67319
67491
  });
67320
67492
  if (isJsonMode()) {
67321
67493
  outputSuccess({
@@ -67332,7 +67504,7 @@ inspectCommand.command("render").description("Inspect a rendered project video w
67332
67504
  exitWithError(generalError(`Render inspection failed: ${error instanceof Error ? error.message : String(error)}`));
67333
67505
  }
67334
67506
  });
67335
- applyTier(inspectCommand.commands[inspectCommand.commands.length - 1], "free");
67507
+ applyTier(inspectCommand.commands[inspectCommand.commands.length - 1], "low");
67336
67508
  inspectCommand.command("media").description("Analyze any media: images, videos, or YouTube URLs using Gemini").argument("<source>", "Image/video file path, image URL, or YouTube URL").argument("<prompt>", "Analysis prompt (e.g., 'Describe this image', 'Summarize this video')").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash").option("--fps <number>", "Frames per second for video (default: 1)").option("--start <seconds>", "Start offset in seconds (video only)").option("--end <seconds>", "End offset in seconds (video only)").option("--low-res", "Use low resolution mode (fewer tokens)").option("--verbose", "Show token usage").option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)").option("--dry-run", "Preview parameters without executing").action(async (source, prompt3, options) => {
67337
67509
  const startedAt = Date.now();
67338
67510
  try {
@@ -67431,6 +67603,12 @@ function printInspectSummary(title, status, score, issues, reportPath) {
67431
67603
  if (issue.suggestedFix) console.log(` ${source_default.dim(issue.suggestedFix)}`);
67432
67604
  }
67433
67605
  }
67606
+ function parseRenderAiModel(value) {
67607
+ if (VALID_RENDER_AI_MODELS.includes(value)) {
67608
+ return value;
67609
+ }
67610
+ exitWithError(usageError(`Invalid --model: ${value}`, `Must be one of: ${VALID_RENDER_AI_MODELS.join(", ")}`));
67611
+ }
67434
67612
  inspectCommand.command("video").description("Analyze video using Gemini (summarize, Q&A, extract info)").argument("<source>", "Video file path or YouTube URL").argument("<prompt>", "Analysis prompt (e.g., 'Summarize this video')").option("-k, --api-key <key>", "Google API key (or set GOOGLE_API_KEY env)").option("-m, --model <model>", "Model: flash (default), flash-2.5, pro", "flash").option("--fps <number>", "Frames per second (default: 1, higher for action)").option("--start <seconds>", "Start offset in seconds (for clipping)").option("--end <seconds>", "End offset in seconds (for clipping)").option("--low-res", "Use low resolution mode (fewer tokens, longer videos)").option("--verbose", "Show token usage").option("--fields <fields>", "Comma-separated fields to include in output (e.g., response,model)").option("--dry-run", "Preview parameters without executing").action(async (source, prompt3, options) => {
67435
67613
  const startedAt = Date.now();
67436
67614
  try {
@@ -76504,7 +76682,8 @@ var contextCommand = new Command("context").description("Print CLI context/guide
76504
76682
  "build",
76505
76683
  "inspect project",
76506
76684
  "render",
76507
- "inspect render --cheap"
76685
+ "inspect render --cheap",
76686
+ "inspect render --ai when needed"
76508
76687
  ],
76509
76688
  mentalModel: {
76510
76689
  storyboard: "intent layer; edit or mutate beat cues here",
@@ -76520,7 +76699,7 @@ var contextCommand = new Command("context").description("Print CLI context/guide
76520
76699
  ],
76521
76700
  semanticFixes: "host-agent",
76522
76701
  mechanicalFixes: "vibe scene repair; vibe scene lint --fix remains the lower-level primitive",
76523
- publicFlow: "vibe init --from <brief> -> edit STORYBOARD.md/DESIGN.md -> vibe storyboard validate -> vibe plan -> vibe build --dry-run --max-cost <usd> -> vibe build -> vibe inspect project -> vibe render -> vibe inspect render --cheap"
76702
+ publicFlow: "vibe init --from <brief> -> edit STORYBOARD.md/DESIGN.md -> vibe storyboard validate -> vibe plan -> vibe build --dry-run --max-cost <usd> -> vibe build -> vibe inspect project -> vibe render -> vibe inspect render --cheap -> optional vibe inspect render --ai"
76524
76703
  };
76525
76704
  if (options.json) {
76526
76705
  console.log(JSON.stringify(contract, null, 2));
@@ -76537,8 +76716,9 @@ var contextCommand = new Command("context").description("Print CLI context/guide
76537
76716
  Source of truth: \`STORYBOARD.md\`, \`DESIGN.md\`, and \`vibe.config.json\`.
76538
76717
  \`STORYBOARD.md\` is the intent layer. Generated scene files under
76539
76718
  \`compositions/\` are artifact layer. Use \`vibe storyboard *\` for narrow
76540
- cue edits; use \`vibe inspect project\`, \`vibe inspect render --cheap\`, and
76541
- \`vibe scene repair\` for deterministic local review and mechanical fixes.
76719
+ cue edits; use \`vibe inspect project\`, \`vibe inspect render --cheap\`,
76720
+ \`vibe inspect render --ai\`, and \`vibe scene repair\` for local and AI
76721
+ review plus mechanical fixes.
76542
76722
  Semantic creative fixes belong to the host agent.
76543
76723
 
76544
76724
  Canonical flow:
@@ -76552,6 +76732,7 @@ vibe build my-video --max-cost 5 --json
76552
76732
  vibe inspect project my-video --json
76553
76733
  vibe render my-video --json
76554
76734
  vibe inspect render my-video --cheap --json
76735
+ vibe inspect render my-video --ai --json
76555
76736
  \`\`\`
76556
76737
 
76557
76738
  Provider precedence: CLI flag -> storyboard cue -> \`vibe.config.json\` ->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/cli",
3
- "version": "0.98.0",
3
+ "version": "0.99.0",
4
4
  "description": "VibeFrame CLI - natural language video editing from the terminal",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -183,8 +183,8 @@
183
183
  "tsx": "^4.21.0",
184
184
  "typescript": "^5.3.3",
185
185
  "vitest": "^1.2.2",
186
- "@vibeframe/ai-providers": "0.98.0",
187
- "@vibeframe/core": "0.98.0"
186
+ "@vibeframe/core": "0.99.0",
187
+ "@vibeframe/ai-providers": "0.99.0"
188
188
  },
189
189
  "engines": {
190
190
  "node": ">=20"