From bbbd5b8355dd83a5cec66b199c7e239c0a9ad12c Mon Sep 17 00:00:00 2001 From: Aleksey Bykhun Date: Fri, 23 Jan 2026 23:15:53 -0800 Subject: [PATCH] feat(react): add Raw element for custom ffmpeg commands - add Raw element type and props with inputs, args, and output - export Raw component and RawProps from react package - support Raw elements as Video src (nested raw commands) - add renderRaw import to video renderer --- src/react/elements.ts | 9 ++++++ src/react/index.ts | 2 ++ src/react/renderers/raw.ts | 61 ++++++++++++++++++++++++++++++++++++ src/react/renderers/video.ts | 6 +++- src/react/types.ts | 19 +++++++++-- 5 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 src/react/renderers/raw.ts diff --git a/src/react/elements.ts b/src/react/elements.ts index 2560d9f6..5c38f8fe 100644 --- a/src/react/elements.ts +++ b/src/react/elements.ts @@ -5,6 +5,7 @@ import type { MusicProps, OverlayProps, PackshotProps, + RawProps, RenderProps, SliderProps, SpeechProps, @@ -139,3 +140,11 @@ export function Swipe(props: SwipeProps): VargElement<"swipe"> { export function Packshot(props: PackshotProps): VargElement<"packshot"> { return createElement("packshot", props as Record, undefined); } + +export function Raw(props: RawProps): VargElement<"raw"> { + return createElement( + "raw", + props as unknown as Record, + undefined, + ); +} diff --git a/src/react/index.ts b/src/react/index.ts index 0efb9fd4..e757da1b 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -7,6 +7,7 @@ export { Music, Overlay, Packshot, + Raw, Render, Slider, Speech, @@ -27,6 +28,7 @@ export type { OverlayProps, PackshotProps, PositionProps, + RawProps, RenderOptions, RenderProps, SliderProps, diff --git a/src/react/renderers/raw.ts b/src/react/renderers/raw.ts new file mode 100644 index 00000000..7ded1824 --- /dev/null +++ b/src/react/renderers/raw.ts @@ -0,0 +1,61 @@ +import { $ } from "bun"; +import { File } from "../../ai-sdk/file"; +import type { RawInput, RawProps, VargElement } from "../types"; +import type { RenderContext } from "./context"; +import { renderImage } from "./image"; +import { renderVideo } from "./video"; + +async function resolveInput( + input: RawInput, + ctx: RenderContext, +): Promise { + if (typeof input === "string") { + return input; + } + + if (input instanceof Uint8Array) { + const tempPath = await File.toTemp({ uint8Array: input }); + ctx.tempFiles.push(tempPath); + return tempPath; + } + + if (input.type === "image") { + return renderImage(input as VargElement<"image">, ctx); + } + + if (input.type === "video") { + return renderVideo(input as VargElement<"video">, ctx); + } + + if (input.type === "raw") { + return renderRaw(input as VargElement<"raw">, ctx); + } + + throw new Error(`Unsupported Raw input type: ${(input as VargElement).type}`); +} + +export async function renderRaw( + element: VargElement<"raw">, + ctx: RenderContext, +): Promise { + const props = element.props as unknown as RawProps; + + const inputPaths = await Promise.all( + props.inputs.map((input) => resolveInput(input, ctx)), + ); + + const outPath = props.output ?? `/tmp/varg-raw-${Date.now()}.mp4`; + + const inputArgs = inputPaths.flatMap((path) => ["-i", path]); + const ffmpegArgs = [...inputArgs, ...props.args, "-y", outPath]; + + const result = + await $`ffmpeg -hide_banner -loglevel error ${ffmpegArgs}`.quiet(); + + if (result.exitCode !== 0) { + throw new Error(`ffmpeg failed with exit code ${result.exitCode}`); + } + + ctx.tempFiles.push(outPath); + return outPath; +} diff --git a/src/react/renderers/video.ts b/src/react/renderers/video.ts index 0c514db4..f14b845f 100644 --- a/src/react/renderers/video.ts +++ b/src/react/renderers/video.ts @@ -9,6 +9,7 @@ import type { import type { RenderContext } from "./context"; import { renderImage } from "./image"; import { addTask, completeTask, startTask } from "./progress"; +import { renderRaw } from "./raw"; import { renderSpeech } from "./speech"; import { computeCacheKey, toFileUrl } from "./utils"; @@ -107,7 +108,10 @@ export async function renderVideo( const props = element.props as VideoProps; if (props.src && !props.prompt) { - return props.src; + if (typeof props.src === "object" && props.src.type === "raw") { + return renderRaw(props.src, ctx); + } + return props.src as string; } const prompt = props.prompt; diff --git a/src/react/types.ts b/src/react/types.ts index 06a0f822..c8c38abe 100644 --- a/src/react/types.ts +++ b/src/react/types.ts @@ -23,7 +23,8 @@ export type VargElementType = | "split" | "slider" | "swipe" - | "packshot"; + | "packshot" + | "raw"; export interface VargElement { type: T; @@ -115,7 +116,7 @@ export type VideoProps = BaseProps & AudioProps & TrimProps & { prompt?: VideoPrompt; - src?: string; + src?: string | VargElement<"raw">; model?: VideoModelV3; resize?: ResizeMode; aspectRatio?: `${number}:${number}`; @@ -202,6 +203,19 @@ export interface PackshotProps extends BaseProps { duration?: number; } +export type RawInput = + | string + | Uint8Array + | VargElement<"image"> + | VargElement<"video"> + | VargElement<"raw">; + +export interface RawProps extends BaseProps { + inputs: RawInput[]; + args: string[]; + output?: string; +} + export type RenderMode = "strict" | "preview"; export interface DefaultModels { @@ -236,4 +250,5 @@ export interface ElementPropsMap { slider: SliderProps; swipe: SwipeProps; packshot: PackshotProps; + raw: RawProps; }