Skip to content

feat: add prerender command — slideshow video with real speech + images#187

Open
SecurityQQ wants to merge 1 commit intomainfrom
feat/prerender
Open

feat: add prerender command — slideshow video with real speech + images#187
SecurityQQ wants to merge 1 commit intomainfrom
feat/prerender

Conversation

@SecurityQQ
Copy link
Copy Markdown
Contributor

Summary

  • Adds varg prerender command that generates a slideshow video for visual-audio sync review before expensive video generation
  • Real speech + real images + real music + real captions, but video generation (Kling, Wan, etc.) is replaced with still-frame images
  • Parametric --image-model flag (default: nano-banana-2) controls which model generates images for text-to-video clips
  • Image-to-video clips use the input image directly as the still frame

New files

  • src/ai-sdk/middleware/prerender.ts — video middleware that intercepts video generation, extracts/generates images, creates still-frame videos via ffmpeg
  • src/cli/commands/prerender.tsx — CLI command with --image-model flag and full help view

Modified files

  • src/react/types.tsRenderMode extended with "prerender", DefaultModels extended with optional prerenderImage
  • src/ai-sdk/middleware/wrap-video-model.tsRenderMode type updated
  • src/react/renderers/render.tsrenderRoot() handles prerender mode (bypasses video cache, wraps model with prerender middleware)
  • src/cli/commands/render.tsx — exports loadComponent, detectDefaultModels, sharedArgs for reuse
  • src/ai-sdk/middleware/index.ts — exports prerender middleware
  • src/cli/commands/index.ts — exports prerender command
  • src/cli/index.ts — registers prerender in subCommands and help routing

Usage

# Default (uses nano-banana-2 for t2v replacement)
varg prerender video.tsx

# Custom image model
varg prerender video.tsx --image-model flux-schnell

# Open after render
varg prerender video.tsx --open

Cost comparison

Mode Cost Time
preview $0 ~5s
prerender ~$0.50 ~30s
render ~$3-5 ~10min

Tested

  • Verified with mike-yan birthday workflow (8 clips, 4 VEED lipsync, 4 Kling)
  • Output: 5.7MB slideshow, 35s, 1080x1920, real speech + captions + music
  • All images/speech hit cache from prior renders; video gen correctly bypassed

…es, no video gen

Adds a new 'varg prerender' command that generates a slideshow video for
visual-audio sync review before expensive video generation.

Prerender mode:
- Real speech generation (ElevenLabs)
- Real image generation for <Image> elements
- Still-frame images for <Video> elements (t2v uses configurable image
  model, i2v uses input image directly)
- Real music generation
- Real captions (Groq Whisper)
- Exact clip durations preserved
- ffmpeg assembly with transitions

The --image-model flag controls which model replaces t2v video generation
(default: nano-banana-2). Example: --image-model flux-schnell

New files:
- src/ai-sdk/middleware/prerender.ts — video middleware that intercepts
  video generation and creates still-frame videos from images
- src/cli/commands/prerender.tsx — CLI command with --image-model flag

Modified files:
- RenderMode type extended with 'prerender'
- DefaultModels extended with optional prerenderImage
- renderRoot() handles prerender mode (bypasses video cache)
- render.tsx exports shared utilities for reuse
- CLI index registers prerender command

Cost savings: ~$0.50 prerender vs ~$3-5 full render for typical workflow.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

walkthrough

introduces a new "prerender" render mode that generates still-frame mp4 videos from images as a fallback mechanism. adds middleware, cli command, type definitions, and render pipeline updates to support this mode with optional image model configuration.

changes

cohort / file(s) summary
middleware system
src/ai-sdk/middleware/index.ts, src/ai-sdk/middleware/prerender.ts, src/ai-sdk/middleware/wrap-video-model.ts
new prerenderFallbackMiddleware wraps video generation to return still-frame mp4s; extracts images from input or generates them; uses ffmpeg with temp files for conversion. RenderMode type extended with "prerender" option.
cli command infrastructure
src/cli/commands/index.ts, src/cli/commands/prerender.tsx, src/cli/commands/render.tsx, src/cli/index.ts
new varg prerender command with --image-model option; resolves image models via varg/fal api keys or defaults; registers command in cli dispatcher with help routing. exports detectDefaultModels, loadComponent, and sharedArgs from render module.
render pipeline & types
src/react/renderers/render.ts, src/react/types.ts
renderRoot adds prerender mode handling with uncached video generation and prerenderFallbackMiddleware; DefaultModels gains optional prerenderImage field; console logging for prerender mode.

sequence diagram

sequenceDiagram
    actor user as user/cli
    participant cli as prerender cmd
    participant loader as component loader
    participant models as model resolver
    participant render as renderroot
    participant middleware as prerenderFallbackMiddleware
    participant imggen as image generator
    participant imgvideo as imageToStillVideo
    participant ffmpeg as ffmpeg

    user->>cli: run `varg prerender`
    cli->>loader: loadComponent(file)
    loader-->>cli: varg element
    cli->>models: detectDefaultModels()
    cli->>models: resolvePrerenderImageModel()
    models-->>cli: image model
    cli->>render: render(mode="prerender", defaults.prerenderImage)
    render->>middleware: wrapVideoModel(prerenderFallbackMiddleware)
    middleware->>middleware: extract/find image from input
    alt has input image (i2v)
        middleware-->>imgvideo: use input image
    else no input (t2v)
        middleware->>imggen: generateImage(prompt)
        imggen-->>middleware: image bytes
        middleware-->>imgvideo: use generated image
    end
    imgvideo->>imgvideo: write png to temp dir
    imgvideo->>ffmpeg: run ffmpeg (loop, scale, pad)
    ffmpeg-->>imgvideo: mp4 bytes
    imgvideo->>imgvideo: cleanup temp files
    imgvideo-->>middleware: still-frame mp4
    middleware-->>render: video response + warning
    render-->>user: output mp4 file
Loading

estimated code review effort

🎯 4 (complex) | ⏱️ ~50 minutes

possibly related prs

poem

stills from motion, frames from thought 📹✨
ffmpeg whispers what cameras caught
when videos hide, images rise
prerender mode in disguise 🎬

meow 🐱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed title clearly describes the main addition: a new prerender command that creates slideshow videos using real images and speech instead of full video generation.
Description check ✅ Passed description is directly related to the changeset, explaining the prerender feature, its purpose, new files, modified files, usage examples, and testing results.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prerender

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/commands/render.tsx (1)

84-97: ⚠️ Potential issue | 🟠 Major

the import detection here is too narrow

hasRelativeImport only catches from "./", so ../foo and side-effect relative imports still fall into the temp-copy path, where those specifiers now resolve relative to .cache/varg-render. and because this branch returns early, files that mix local imports with vargai/* skip the rewrite block entirely. that's going to break a bunch of normal component layouts.

Also applies to: 99-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/render.tsx` around lines 84 - 97, The current
hasRelativeImport check is too narrow (only looks for "from './") and the early
return causes files that mix local relative imports with "vargai/*" to skip the
rewrite; update the detection to match any relative import specifier (both "./"
and "../") including side-effect imports (e.g. use a regex like
/from\s+['"](?:\.{1,2}\/)|import\s+['"](?:\.{1,2}\/)/m or equivalent) so
hasRelativeImport becomes true for all "./" or "../" specifiers, and remove the
early return that exits before the rewrite block — instead, when
hasRelativeImport is true still import via import(resolvedPath) where needed but
allow the subsequent rewrite logic to run (references: hasRelativeImport,
resolvedPath, tmpDir, resolveDefaultExport).
🧹 Nitpick comments (3)
src/cli/commands/render.tsx (1)

17-19: add jsdoc now that these helpers are public

detectDefaultModels() and loadComponent() just became reusable exports, but they still ship without api docs.

As per coding guidelines, "Ensure all public functions and classes have JSDoc comments".

Also applies to: 73-73

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/render.tsx` around lines 17 - 19, Add JSDoc comments for the
newly exported helpers detectDefaultModels and loadComponent (and any other
exported symbols like the DefaultModels type) describing their purpose, inputs,
and return values; place a short summary line, `@returns` with the
Promise<DefaultModels | undefined> for detectDefaultModels, and for
loadComponent include parameter descriptions and the return type. Ensure the
JSDoc appears immediately above each exported function declaration
(detectDefaultModels, loadComponent) and follows project style (brief
description, `@param` for each arg if any, `@returns`, and `@example` if useful).
src/react/types.ts (1)

338-338: tighten the new prerender type surface

RenderMode now lives here and again in src/ai-sdk/middleware/wrap-video-model.ts, while prerenderImage is only enforced by a late runtime throw. pulling this into one shared discriminated type would keep the modes in sync and catch bad callers before renderRoot() explodes.

Also applies to: 346-347

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/react/types.ts` at line 338, Consolidate the RenderMode union into a
shared discriminated type (e.g., export type RenderMode = { mode: "strict" } | {
mode: "preview" } | { mode: "prerender"; prerenderImage: true }) in a common
types module and import that type wherever RenderMode currently appears
(including src/react/types.ts and src/ai-sdk/middleware/wrap-video-model.ts);
update usages in renderRoot, prerenderImage checks, and any callers to use the
discriminant (.mode) so TypeScript enforces that only the "prerender" variant
can carry prerenderImage, and remove the late runtime throw by making the
compiler require the prerenderImage property on the prerender variant.
src/cli/commands/prerender.tsx (1)

226-228: add jsdoc for the exported helper

showPrerenderHelp is public (exported) and currently undocumented. add a short jsdoc block to match repo standards.

proposed fix
+/**
+ * render the static help view for the `varg prerender` command.
+ */
 export function showPrerenderHelp() {
   renderStatic(<PrerenderHelpView />);
 }

as per coding guidelines, "**/*.{js,jsx,ts,tsx}: Ensure all public functions and classes have JSDoc comments".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/prerender.tsx` around lines 226 - 228, Add a JSDoc block
above the exported function showPrerenderHelp describing its purpose (renders
the prerender help view to static output), noting it takes no parameters and
returns void; reference the function name showPrerenderHelp and the fact it
calls renderStatic(<PrerenderHelpView />) so the doc aligns with the
implementation and repo JSDoc standards for public functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ai-sdk/middleware/prerender.ts`:
- Around line 100-129: extractFirstImage currently only preserves image files
and silently falls through for audio/video inputs; update extractFirstImage (and
the same logic at the other occurrence) to explicitly handle non-image media:
detect mediaType starting with "video/" and "audio/"; for "video/" try to
extract a poster or first frame (e.g., call a new helper like
extractFirstFrameFromVideo(file.data) or use file.metadata?.poster if present
and return its Uint8Array), and for "audio/" (or when video frame extraction
fails) fail fast by throwing a clear error (or return a distinct failure) so we
do not silently synthesize a T2V frame—add/implement
extractFirstFrameFromVideo(data: Uint8Array | string): Promise<Uint8Array |
undefined> and use it inside extractFirstImage for file.type === "file" and
mediaType.startsWith("video/"), otherwise throw an error describing the
unsupported non-image input.

In `@src/cli/commands/prerender.tsx`:
- Around line 45-50: The code that derives basename using
file.replace(...).split("/").pop() is not path-safe and breaks on Windows paths;
update the logic in the prerender command to use Node's path utilities (import {
basename as pathBasename } from "node:path" or similar) to compute
basename(file) after stripping the .tsx? extension, then use that safe basename
when building outputPath (the variables to change are basename and outputPath in
prerender.tsx).

---

Outside diff comments:
In `@src/cli/commands/render.tsx`:
- Around line 84-97: The current hasRelativeImport check is too narrow (only
looks for "from './") and the early return causes files that mix local relative
imports with "vargai/*" to skip the rewrite; update the detection to match any
relative import specifier (both "./" and "../") including side-effect imports
(e.g. use a regex like /from\s+['"](?:\.{1,2}\/)|import\s+['"](?:\.{1,2}\/)/m or
equivalent) so hasRelativeImport becomes true for all "./" or "../" specifiers,
and remove the early return that exits before the rewrite block — instead, when
hasRelativeImport is true still import via import(resolvedPath) where needed but
allow the subsequent rewrite logic to run (references: hasRelativeImport,
resolvedPath, tmpDir, resolveDefaultExport).

---

Nitpick comments:
In `@src/cli/commands/prerender.tsx`:
- Around line 226-228: Add a JSDoc block above the exported function
showPrerenderHelp describing its purpose (renders the prerender help view to
static output), noting it takes no parameters and returns void; reference the
function name showPrerenderHelp and the fact it calls
renderStatic(<PrerenderHelpView />) so the doc aligns with the implementation
and repo JSDoc standards for public functions.

In `@src/cli/commands/render.tsx`:
- Around line 17-19: Add JSDoc comments for the newly exported helpers
detectDefaultModels and loadComponent (and any other exported symbols like the
DefaultModels type) describing their purpose, inputs, and return values; place a
short summary line, `@returns` with the Promise<DefaultModels | undefined> for
detectDefaultModels, and for loadComponent include parameter descriptions and
the return type. Ensure the JSDoc appears immediately above each exported
function declaration (detectDefaultModels, loadComponent) and follows project
style (brief description, `@param` for each arg if any, `@returns`, and `@example` if
useful).

In `@src/react/types.ts`:
- Line 338: Consolidate the RenderMode union into a shared discriminated type
(e.g., export type RenderMode = { mode: "strict" } | { mode: "preview" } | {
mode: "prerender"; prerenderImage: true }) in a common types module and import
that type wherever RenderMode currently appears (including src/react/types.ts
and src/ai-sdk/middleware/wrap-video-model.ts); update usages in renderRoot,
prerenderImage checks, and any callers to use the discriminant (.mode) so
TypeScript enforces that only the "prerender" variant can carry prerenderImage,
and remove the late runtime throw by making the compiler require the
prerenderImage property on the prerender variant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53a4e020-cba0-4684-afec-201c5083c654

📥 Commits

Reviewing files that changed from the base of the PR and between 40813d4 and 92ec545.

📒 Files selected for processing (9)
  • src/ai-sdk/middleware/index.ts
  • src/ai-sdk/middleware/prerender.ts
  • src/ai-sdk/middleware/wrap-video-model.ts
  • src/cli/commands/index.ts
  • src/cli/commands/prerender.tsx
  • src/cli/commands/render.tsx
  • src/cli/index.ts
  • src/react/renderers/render.ts
  • src/react/types.ts

Comment on lines +100 to +129
async function extractFirstImage(
params: VideoModelV3CallOptions,
): Promise<Uint8Array | undefined> {
if (!params.files) return undefined;

for (const file of params.files) {
if (file.type === "file" && file.mediaType?.startsWith("image/")) {
if (file.data instanceof Uint8Array) {
return file.data;
}
if (typeof file.data === "string") {
// base64
return Uint8Array.from(atob(file.data), (c) => c.charCodeAt(0));
}
}
if (file.type === "url") {
// Fetch the URL to get binary data
try {
const response = await fetch(file.url);
const contentType = response.headers.get("content-type") ?? "";
if (contentType.startsWith("image/")) {
return new Uint8Array(await response.arrayBuffer());
}
} catch {
// Skip URLs that can't be fetched
}
}
}

return undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

non-image source inputs get dropped here

the public VideoPrompt already allows audio and video sources, but this path only preserves image files. when params.files contains a video/audio input, extractFirstImage() falls through and we synthesize a fresh t2v frame from the prompt instead, which changes the scene you're trying to review. at minimum, fail fast for unsupported params.files; ideally extract a poster or first frame for video inputs.

Also applies to: 152-179

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ai-sdk/middleware/prerender.ts` around lines 100 - 129, extractFirstImage
currently only preserves image files and silently falls through for audio/video
inputs; update extractFirstImage (and the same logic at the other occurrence) to
explicitly handle non-image media: detect mediaType starting with "video/" and
"audio/"; for "video/" try to extract a poster or first frame (e.g., call a new
helper like extractFirstFrameFromVideo(file.data) or use file.metadata?.poster
if present and return its Uint8Array), and for "audio/" (or when video frame
extraction fails) fail fast by throwing a clear error (or return a distinct
failure) so we do not silently synthesize a T2V frame—add/implement
extractFirstFrameFromVideo(data: Uint8Array | string): Promise<Uint8Array |
undefined> and use it inside extractFirstImage for file.type === "file" and
mediaType.startsWith("video/"), otherwise throw an error describing the
unsupported non-image input.

Comment on lines +45 to +50
const basename = file
.replace(/\.tsx?$/, "")
.split("/")
.pop();
const outputPath =
(args.output as string) ?? `output/${basename}-prerender.mp4`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

use path-safe filename extraction for default output path

line 45-line 48 uses split("/"), which is unix-only. with windows-style paths, the generated default output can be malformed (for example includes drive/path fragments). use node:path basename extraction instead.

proposed fix
-import { dirname } from "node:path";
+import { basename, dirname } from "node:path";
@@
-    const basename = file
-      .replace(/\.tsx?$/, "")
-      .split("/")
-      .pop();
+    const fileStem = basename(file).replace(/\.tsx?$/, "");
     const outputPath =
-      (args.output as string) ?? `output/${basename}-prerender.mp4`;
+      (args.output as string) ?? `output/${fileStem}-prerender.mp4`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const basename = file
.replace(/\.tsx?$/, "")
.split("/")
.pop();
const outputPath =
(args.output as string) ?? `output/${basename}-prerender.mp4`;
import { basename, dirname } from "node:path";
...
const fileStem = basename(file).replace(/\.tsx?$/, "");
const outputPath =
(args.output as string) ?? `output/${fileStem}-prerender.mp4`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cli/commands/prerender.tsx` around lines 45 - 50, The code that derives
basename using file.replace(...).split("/").pop() is not path-safe and breaks on
Windows paths; update the logic in the prerender command to use Node's path
utilities (import { basename as pathBasename } from "node:path" or similar) to
compute basename(file) after stripping the .tsx? extension, then use that safe
basename when building outputPath (the variables to change are basename and
outputPath in prerender.tsx).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant