New: TranslatePsy-AfriSLM translates directly between 19 African languages, offline.
QVAC Logo
SDKVideo generation
v0.21, the current release

Video generation

Text-to-video and image-to-video generation using a customized Diffusion engine.

Overview

Video generation runs on a customized Diffusion engine (qvac-ext-stable-diffusion.cpp). Load a supported model using modelType: "diffusion" with modelConfig.mode: "video". Then call video() with a mode and prompt.

It supports two modes — txt2vid (from a text prompt) and img2vid (animate a still image) — across the WAN 2.1, Wan 2.2, LTX-2, and MiniMax-H3 model families. video() returns { progressStream, outputs, stats }: outputs resolves to the generated video(s) as Uint8Array buffers (AVI), progressStream streams step-by-step progress, and stats carries per-run metadata.

Video generation is hardware-intensive: it requires at least 16 GB of video memory or 20 GB of unified memory.

Functions

Use the following sequence of function calls:

  1. loadModel()
  2. video()
  3. unloadModel()

For how to use each function, see SDK — API reference.

video()

Generates a video from a prompt in one of two modes:

Modes

  • txt2vid — generate a video from a text prompt alone. Supported by all families.
  • img2vid — animate a still image. Supported by WAN (load the model with clipVisionModelSrc, an OpenCLIP ViT-H/14 encoder) and LTX-2 (no CLIP-vision encoder needed).

img2vid takes two extra params:

  • init_image — PNG or JPEG bytes (Uint8Array) of the first frame. Required.
  • strength — a value in 0–1. Use higher values when you want the output to diverge more from the first frame.

Generation knobs

Common to all families:

  • video_frames — number of frames to generate. Wan models use 4k + 1 (e.g. 17, 33, 49, 81). LTX-2 uses 8k + 1 (e.g. 25, 121, 241). MiniMax-H3 uses 17k + 5 (e.g. 5, 22, 39, 56, 73, 90, 107, 124).
  • fps — output frame rate.
  • cfg_scale — classifier-free guidance strength. Higher values follow the prompt more closely.
  • flow_shift — flow-matching shift. For Wan 2.1 T2V, use 3.0; higher values can produce near-static frames.

LTX-2 adds:

  • temporal_tiling — set to true to cap peak VRAM on HD or long clips. No effect on WAN.

LTX-2 reference conditioning (IC-LoRA)

Available in txt2vid only. Pass a composite reference sheet together with an Ingredients LoRA so the generated video inherits a specific character or style:

  • scheduler — set to 'ltx2'.
  • reference_images — one composite reference sheet as a Uint8Array. Required to enable reference conditioning.
  • lora — worker-local absolute path to an Ingredients LoRA adapter. Required alongside reference_images.
  • lora_strength — controls how strongly the LoRA steers appearance.
  • reference_attention_strength — controls how closely the video follows the reference.
  • stg_scale, stg_block — optional; tune them to refine output.
  • reference_downscale_factor — must be 1.

Wan 2.2 A14B dual-expert knobs

Wan 2.2 A14B routes a second set of knobs to its high-noise expert. They require a model loaded with modelConfig.highNoiseDiffusionModelSrc and are rejected on single-expert layouts (Wan 2.1, Wan 2.2 TI2V-5B) with PluginRequestValidationFailedError:

  • high_noise_steps — steps for the high-noise expert.
  • high_noise_sample_method — sampling method for the high-noise expert.
  • high_noise_scheduler — scheduler for the high-noise expert.
  • high_noise_cfg_scale — classifier-free guidance strength for the high-noise expert.
  • high_noise_flow_shift — flow-matching shift for the high-noise expert.
  • moe_boundary — boundary at which sampling switches from the high-noise expert to the low-noise expert.

Size and frame constraints:

  • width and height must be positive multiples of 16.
  • On LTX-2 and Wan 2.2 TI2V-5B, width and height must be multiples of 32.
  • On LTX-2, video_frames must satisfy 8*k + 1 (e.g. 25, 121, 241), with a maximum of 257.
  • On LTX-2 with reference_images, the minimum video_frames rises to 121.
  • On MiniMax-H3, width and height must be multiples of 32, video_frames must satisfy 17*k + 5, and fps must be 24. H3 produces synchronized audio at 32 kHz.

The returned stats includes hasAudio (true when the output AVI carries a muxed audio track) and audioSampleRate (Hz of that track, 0 when there is no audio).

Models

Four model families are supported — WAN 2.1, Wan 2.2, LTX-2, and MiniMax-H3 — and the file layout is auto-selected at load time from the model sources you provide:

  • WAN 2.1 T2V (txt2vid): split layout — diffusion model + UMT5-XXL text encoder (via t5XxlModelSrc) + VAE (via vaeModelSrc). Available constants: WAN2_1_T2V_1_3B_FP16, UMT5_XXL_FP16, WAN_2_1_COMFYUI_REPACKAGED_VAE.
  • WAN 2.1 I2V (img2vid): same split layout as T2V, plus an OpenCLIP ViT-H/14 vision encoder (via clipVisionModelSrc). Available constants: WAN2_1_I2V_14B_Q4_K_M, CLIP_VISION_H, UMT5_XXL_FP16, WAN_2_1_COMFYUI_REPACKAGED_VAE.
  • Wan 2.2 TI2V-5B (txt2vid, single-expert): split layout — diffusion model + UMT5-XXL text encoder (via t5XxlModelSrc) + Wan 2.2 VAE (via vaeModelSrc). The Wan 2.2 VAE's 16× spatial compression is what gives TI2V its 32-pixel grid; the Wan 2.1 VAE is not interchangeable. The UMT5-XXL text encoder is byte-identical to the Wan 2.1 repackage, so both generations share the one registry entry. Available constants: WAN2_2_TI2V_5B_Q5_K_S, UMT5_XXL_FP16, WAN_2_2_COMFYUI_REPACKAGED_VAE.
  • Wan 2.2 A14B (txt2vid, dual-expert / mixture-of-experts): split layout — low-noise diffusion model + high-noise diffusion model (via highNoiseDiffusionModelSrc, whose presence selects this layout) + UMT5-XXL text encoder + Wan 2.2 VAE. The A14B-only high_noise_* and moe_boundary request fields route to the high-noise expert and are rejected on single-expert layouts.
  • LTX-2 (txt2vid and img2vid): split layout — diffusion model + Gemma text encoder (via llmModelSrc) + video VAE (via vaeModelSrc) + text-embedding connectors (via embeddingsConnectorsModelSrc, whose presence selects this layout), plus an optional audio VAE (via audioVaeModelSrc) for the synchronized 48 kHz audio track. Its img2vid path needs no CLIP-vision encoder.
  • MiniMax-H3 (txt2vid): split layout — diffusion model + Qwen3-VL text encoder (via llmModelSrc) + video VAE (via vaeModelSrc) + audio VAE (via audioVaeModelSrc). H3 uses a 32-pixel spatial grid, 17*k + 5 frames at 24 FPS, and produces synchronized 32 kHz audio. It does not support img2vid, Wan's T5 encoder, or CLIP vision encoders.

LTX-2 is large (~21B) — budget at least 20 GB of video / unified memory.

For models available as constants, see SDK — Models.

Examples

Text-to-video (WAN 2.1)

The following script shows text-to-video generation using Wan 2.1 T2V 1.3B with its split-layout model (separate diffusion model, UMT5-XXL text encoder, and VAE):

diffusion-txt2vid.js
import { loadModel, unloadModel, video, WAN2_1_T2V_1_3B_FP16, UMT5_XXL_FP16, WAN_2_1_COMFYUI_REPACKAGED_VAE } from '@qvac/sdk';
import fs from 'fs';
import path from 'path';
// Text-to-video with Wan 2.1 T2V 1.3B. Wan uses a split layout:
// a diffusion model + a UMT5-XXL text encoder + a VAE.
// This example needs powerful hardware: at least 16 GB of video memory or
// 20 GB of unified memory.
const diffusionModelSrc = process.argv[2] || WAN2_1_T2V_1_3B_FP16;
const t5XxlModelSrc = process.argv[3] || UMT5_XXL_FP16;
const vaeModelSrc = process.argv[4] || WAN_2_1_COMFYUI_REPACKAGED_VAE;
// Prompt tip: Wan 1.3B is small and has weak temporal priors. Use motion-
// explicit verbs and avoid static framing words like "standing", "still",
// or "portrait" in the positive prompt.
const prompt = process.argv[5] || 'a colorful bird flapping its wings';
const outputDir = process.argv[6] || '.';
try {
    console.log('▸ Loading Wan 2.1 T2V model (diffusion + UMT5-XXL + VAE)...');
    const modelId = await loadModel({
        modelSrc: diffusionModelSrc,
        modelType: 'sdcpp-generation',
        modelConfig: {
            mode: 'video',
            device: 'gpu',
            threads: 4,
            t5XxlModelSrc,
            vaeModelSrc,
            diffusion_fa: true,
            offload_to_cpu: true,
            params_backend: 'vae=cpu',
            vae_tiling: true
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log(`\n▸ Generating video for: "${prompt}"`);
    const { progressStream, outputs, stats } = video({
        modelId,
        mode: 'txt2vid',
        prompt,
        negative_prompt: 'blurry, low quality, static, jittery, watermark',
        width: 480,
        height: 832,
        // Frame count must satisfy (4*k + 1), k >= 1. Common values at 16 fps:
        // 17 frames ~= 1.06s (very fast, ~6 min on M3 Ultra Metal)
        // 33 frames ~= 2.06s (default in this example, ~11 min)
        // 49 frames ~= 3.06s (~17 min)
        // 65 frames ~= 4.06s (~22 min)
        // 81 frames ~= 5.06s (Wan 1.3B native training length, best motion
        // quality, ~28 min)
        // Going beyond 81 can degrade quality because it exceeds the model's
        // positional embeddings.
        video_frames: 33,
        fps: 16,
        steps: 30,
        cfg_scale: 6.0,
        // Wan 2.1 T2V needs flow_shift=3.0 for visible motion. Higher values can
        // make consecutive frames near-identical, which looks like a frozen video.
        flow_shift: 3.0,
        seed: 42,
        vae_tiling: true
    });
    for await (const { step, totalSteps } of progressStream) {
        console.log(`▸ step ${step}/${totalSteps}`);
    }
    const buffers = await outputs;
    for (let i = 0; i < buffers.length; i++) {
        const outputPath = path.join(outputDir, `wan_t2v_${i}.avi`);
        fs.writeFileSync(outputPath, buffers[i]);
        console.log(`▸ Saved ${outputPath}`);
    }
    console.log('\n▸ Stats:', await stats);
    await unloadModel({ modelId, clearStorage: false });
    console.log('▸ Done.');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Image-to-video (WAN 2.1)

The following script shows image-to-video generation using Wan 2.1 I2V with its split-layout model (separate diffusion model, UMT5-XXL text encoder, VAE, and CLIP vision encoder). It animates a first-frame init_image guided by a motion prompt:

diffusion-img2vid.js
import { loadModel, unloadModel, video, WAN2_1_I2V_14B_Q4_K_M, CLIP_VISION_H, UMT5_XXL_FP16, WAN_2_1_COMFYUI_REPACKAGED_VAE } from '@qvac/sdk';
import fs from 'fs';
import path from 'path';
// Image-to-video with Wan 2.1 I2V. Requires a Wan I2V diffusion checkpoint (GGUF
// recommended), plus UMT5-XXL, Wan VAE, and CLIP vision weights. The model
// sources default to the bundled registry constants, so the common case is just
// an init image path.
const initImagePath = process.argv[2];
const prompt = process.argv[3] || 'the subject slowly turns and smiles, soft natural lighting, cinematic';
const outputDir = process.argv[4] || '.';
const diffusionModelSrc = process.argv[5] || WAN2_1_I2V_14B_Q4_K_M;
const t5XxlModelSrc = process.argv[6] || UMT5_XXL_FP16;
const vaeModelSrc = process.argv[7] || WAN_2_1_COMFYUI_REPACKAGED_VAE;
const clipVisionModelSrc = process.argv[8] || CLIP_VISION_H;
if (!initImagePath) {
    console.error('✖ init image path is required');
    console.error('Usage: bun run bare:example dist/examples/diffusion-img2vid.js ' +
        '<initImagePath> [prompt] [outputDir] ' +
        '[i2vModelSrc] [t5XxlModelSrc] [vaeModelSrc] [clipVisionModelSrc]');
    process.exit(1);
}
try {
    console.log('▸ Loading Wan 2.1 I2V model (diffusion + UMT5-XXL + VAE + CLIP vision)...');
    const modelId = await loadModel({
        modelSrc: diffusionModelSrc,
        modelType: 'sdcpp-generation',
        modelConfig: {
            mode: 'video',
            device: 'gpu',
            threads: 4,
            t5XxlModelSrc,
            vaeModelSrc,
            clipVisionModelSrc,
            diffusion_fa: true,
            offload_to_cpu: true,
            params_backend: 'vae=cpu',
            vae_tiling: true
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    const init_image = new Uint8Array(fs.readFileSync(initImagePath));
    console.log(`▸ Generating video for: "${prompt}"`);
    const { progressStream, outputs, stats } = video({
        modelId,
        mode: 'img2vid',
        prompt,
        init_image,
        negative_prompt: 'blurry, distorted, low quality, jittery, static, frozen',
        strength: 0.85,
        flow_shift: 3.0,
        video_frames: 33,
        fps: 16,
        steps: 30,
        cfg_scale: 6.0,
        seed: 42,
        vae_tiling: true
    });
    for await (const { step, totalSteps } of progressStream) {
        console.log(`▸ step ${step}/${totalSteps}`);
    }
    const buffers = await outputs;
    for (let i = 0; i < buffers.length; i++) {
        const outputPath = path.join(outputDir, `wan_i2v_${i}.avi`);
        fs.writeFileSync(outputPath, buffers[i]);
        console.log(`▸ Saved ${outputPath}`);
    }
    console.log('▸ Stats:', await stats);
    await unloadModel({ modelId, clearStorage: false });
    console.log('▸ Done');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Text-to-video (Wan 2.2 TI2V-5B)

The following script shows text-to-video generation with Wan 2.2 TI2V-5B Turbo (single-expert). It loads the same split layout as Wan 2.1 T2V — diffusion model + UMT5-XXL text encoder + VAE — but with the Wan 2.2 VAE, whose 16× spatial compression enforces the TI2V 32-pixel grid. high_noise_* and moe_boundary are A14B-only and therefore omitted here:

diffusion-txt2vid-wan22.js
import { loadModel, unloadModel, video, WAN2_2_TI2V_5B_Q5_K_S, UMT5_XXL_FP16, WAN_2_2_COMFYUI_REPACKAGED_VAE } from '@qvac/sdk';
import fs from 'fs';
import path from 'path';
// Text-to-video with Wan 2.2 TI2V-5B Turbo (Q5_K_S). Like Wan 2.1 this is a
// split layout — diffusion model + UMT5-XXL text encoder + VAE — but it needs
// the Wan 2.2 VAE specifically: its 16x spatial compression is what gives
// TI2V its 32-pixel grid, and the Wan 2.1 VAE is not interchangeable.
//
// TI2V-5B is a single-expert model. The high_noise_* / moe_boundary fields
// belong to the two-expert Wan 2.2 A14B layout and are rejected here.
//
// Budget at least 16 GB of video memory or 20 GB of unified memory. The
// artifacts total roughly 16.4 GB, dominated by the fp16 text encoder.
//
// The text encoder is byte-identical to the Wan 2.1 repackage, so both
// generations share the one UMT5_XXL_FP16 registry entry.
const diffusionModelSrc = process.argv[2] || WAN2_2_TI2V_5B_Q5_K_S;
const t5XxlModelSrc = process.argv[3] || UMT5_XXL_FP16;
const vaeModelSrc = process.argv[4] || WAN_2_2_COMFYUI_REPACKAGED_VAE;
// Prompt tip: Turbo responds well to explicit camera and lighting direction.
// Describe continuous motion rather than a pose, or the clip reads as a still.
const prompt = process.argv[5] ||
    'A single white porcelain espresso cup on a dark walnut table beside a sunlit window, ' +
        'delicate steam curling upward, slow circular camera move, warm morning light, ' +
        'sharp ceramic texture, realistic continuous motion';
const outputDir = process.argv[6] || '.';
try {
    console.log('▸ Loading Wan 2.2 TI2V-5B Turbo model (diffusion + UMT5-XXL + Wan 2.2 VAE)...');
    const modelId = await loadModel({
        modelSrc: diffusionModelSrc,
        modelType: 'sdcpp-generation',
        modelConfig: {
            mode: 'video',
            device: 'gpu',
            threads: 4,
            t5XxlModelSrc,
            vaeModelSrc,
            diffusion_fa: true,
            offload_to_cpu: true,
            vae_tiling: true
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log(`\n▸ Generating video for: "${prompt}"`);
    const { progressStream, outputs, stats } = video({
        modelId,
        mode: 'txt2vid',
        prompt,
        negative_prompt: 'flickering, temporal jitter, morphing, duplicated subject, warped geometry, ' +
            'distorted anatomy, blurry details, low resolution, text, watermark, logo',
        // TI2V-5B requires width and height to be multiples of 32 — stricter than
        // the multiple of 16 the schema enforces for Wan generally. Native
        // validation derives this from the loaded GGUF, so a non-conforming size
        // fails at generation time rather than at load.
        // Turbo was trained on this 720p, 24 fps, five-second shape; 640x352 with
        // 49 frames is a much cheaper way to smoke-test the pipeline.
        width: 1280,
        height: 704,
        // Frame count must satisfy (4*k + 1), k >= 1. At 24 fps: 49 ≈ 2s,
        // 121 ≈ 5s (the trained length).
        video_frames: 121,
        fps: 24,
        // Turbo is distilled for very short schedules: 4 steps with guidance
        // effectively disabled. The non-distilled TI2V-5B wants steps >= 30 and
        // cfg ~5.0 instead.
        steps: 4,
        cfg_scale: 1.0,
        flow_shift: 5.0,
        seed: 42,
        vae_tiling: true
    });
    for await (const { step, totalSteps } of progressStream) {
        console.log(`▸ step ${step}/${totalSteps}`);
    }
    const buffers = await outputs;
    for (let i = 0; i < buffers.length; i++) {
        const outputPath = path.join(outputDir, `wan22_ti2v_t2v_${i}.avi`);
        fs.writeFileSync(outputPath, buffers[i]);
        console.log(`▸ Saved ${outputPath}`);
    }
    console.log('\n▸ Stats:', await stats);
    await unloadModel({ modelId, clearStorage: false });
    console.log('▸ Done.');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Text-to-video with audio (LTX-2)

The following script loads LTX-2 with its split layout (diffusion model + Gemma text encoder + video VAE + embedding connectors + optional audio VAE) and generates a clip with a synchronized 48 kHz audio track. It uses the LTX-2 dimension/frame constraints (multiples of 32, 8*k + 1 frames) and enables temporal_tiling to cap peak VRAM:

diffusion-txt2vid-ltx.js
import { loadModel, unloadModel, video } from '@qvac/sdk';
import fs from 'fs';
import path from 'path';
// Text-to-video (+ synchronized audio) with LTX-2. Unlike Wan, LTX-2 uses a
// Gemma text encoder (llmModelSrc), a video VAE (vaeModelSrc), a set of
// text-embedding connectors (embeddingsConnectorsModelSrc — this is what
// selects the LTX-2 layout), and an optional audio VAE (audioVaeModelSrc)
// that produces a 48 kHz audio track muxed into the output AVI.
//
// This example needs powerful hardware: LTX-2.3 is a ~21B model, budget at
// least 20+ GB of video / unified memory.
//
// No LTX-2 model constants ship in the SDK registry, so the model sources
// default to public HuggingFace URLs (the SDK downloads http(s) sources
// directly). Pass your own paths / URLs via argv to override.
const HF = 'https://huggingface.co';
const diffusionModelSrc = process.argv[2] ||
    `${HF}/QuantStack/LTX-2.3-GGUF/resolve/main/LTX-2.3-distilled-1.1/LTX-2.3-22B-distilled-1.1-Q5_K_M.gguf`;
const llmModelSrc = process.argv[3] || `${HF}/unsloth/gemma-3-12b-it-GGUF/resolve/main/gemma-3-12b-it-UD-Q4_K_XL.gguf`;
const vaeModelSrc = process.argv[4] ||
    `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/vae/ltx-2.3-22b-distilled_video_vae.safetensors`;
const audioVaeModelSrc = process.argv[5] ||
    `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/vae/ltx-2.3-22b-distilled_audio_vae.safetensors`;
const embeddingsConnectorsModelSrc = process.argv[6] ||
    `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/text_encoders/ltx-2.3-22b-distilled_embeddings_connectors.safetensors`;
const prompt = process.argv[7] || 'a claymation cat playing jazz on a piano';
const outputDir = process.argv[8] || '.';
try {
    console.log('▸ Loading LTX-2 model (diffusion + Gemma + video VAE + audio VAE + connectors)...');
    const modelId = await loadModel({
        modelSrc: diffusionModelSrc,
        modelType: 'sdcpp-generation',
        modelConfig: {
            mode: 'video',
            device: 'gpu',
            threads: 4,
            // Supplying embeddingsConnectorsModelSrc selects the LTX-2 layout.
            llmModelSrc,
            vaeModelSrc,
            audioVaeModelSrc,
            embeddingsConnectorsModelSrc,
            diffusion_fa: true,
            vae_tiling: true
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    console.log(`\n▸ Generating video for: "${prompt}"`);
    const { progressStream, outputs, stats } = video({
        modelId,
        mode: 'txt2vid',
        prompt,
        negative_prompt: 'blurry, low quality, static, jittery, watermark, distorted audio',
        // LTX-2 requires width/height as multiples of 32.
        width: 512,
        height: 320,
        // LTX-2 requires (8*k + 1) frames, max 257. At 24 fps: 25≈1s, 121≈5s, 241≈10s.
        video_frames: 121,
        fps: 24,
        // Distilled variants run in 4-8 steps with cfg ~1.0; the full --dev model
        // wants steps >= 20 and cfg ~7.0.
        steps: 10,
        cfg_scale: 1.0,
        seed: 42,
        // Tile the video VAE decode along the time axis to cap peak VRAM on long
        // / HD clips (LTX-2 only).
        temporal_tiling: true
    });
    for await (const { step, totalSteps } of progressStream) {
        console.log(`▸ step ${step}/${totalSteps}`);
    }
    const buffers = await outputs;
    for (let i = 0; i < buffers.length; i++) {
        const outputPath = path.join(outputDir, `ltx_t2v_${i}.avi`);
        fs.writeFileSync(outputPath, buffers[i]);
        console.log(`▸ Saved ${outputPath} (play in VLC to hear the muxed audio track)`);
    }
    console.log('\n▸ Stats:', await stats);
    await unloadModel({ modelId, clearStorage: false });
    console.log('▸ Done.');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Image-to-video (LTX-2)

The following script animates a first-frame init_image with LTX-2. Unlike WAN I2V, the LTX-2 img2vid path needs no CLIP-vision encoder:

diffusion-img2vid-ltx.js
import { loadModel, unloadModel, video } from '@qvac/sdk';
import fs from 'fs';
import path from 'path';
// Image-to-video (+ synchronized audio) with LTX-2. Unlike Wan I2V, LTX-2
// conditions on the first frame through its video VAE, so it needs NO CLIP
// vision weights (clipVisionModelSrc) — the same LTX-2 layout used for
// txt2vid works for img2vid, you just add an init_image at generation time.
//
// This example needs powerful hardware: LTX-2.3 is a ~21B model, budget at
// least 20+ GB of video / unified memory.
//
// No LTX-2 model constants ship in the SDK registry, so the model sources
// default to public HuggingFace URLs (the SDK downloads http(s) sources
// directly). Pass your own paths / URLs via argv to override.
const HF = 'https://huggingface.co';
const initImagePath = process.argv[2];
const prompt = process.argv[3] || 'the subject slowly turns and smiles, soft natural lighting, cinematic';
const outputDir = process.argv[4] || '.';
const diffusionModelSrc = process.argv[5] ||
    `${HF}/QuantStack/LTX-2.3-GGUF/resolve/main/LTX-2.3-distilled-1.1/LTX-2.3-22B-distilled-1.1-Q5_K_M.gguf`;
const llmModelSrc = process.argv[6] || `${HF}/unsloth/gemma-3-12b-it-GGUF/resolve/main/gemma-3-12b-it-UD-Q4_K_XL.gguf`;
const vaeModelSrc = process.argv[7] ||
    `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/vae/ltx-2.3-22b-distilled_video_vae.safetensors`;
const audioVaeModelSrc = process.argv[8] ||
    `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/vae/ltx-2.3-22b-distilled_audio_vae.safetensors`;
const embeddingsConnectorsModelSrc = process.argv[9] ||
    `${HF}/unsloth/LTX-2.3-GGUF/resolve/main/text_encoders/ltx-2.3-22b-distilled_embeddings_connectors.safetensors`;
if (!initImagePath) {
    console.error('✖ init image path is required');
    console.error('Usage: bun run bare:example dist/examples/diffusion-img2vid-ltx.js ' +
        '<initImagePath> [prompt] [outputDir] ' +
        '[diffusionModelSrc] [llmModelSrc] [vaeModelSrc] [audioVaeModelSrc] [embeddingsConnectorsModelSrc]');
    process.exit(1);
}
try {
    console.log('▸ Loading LTX-2 model (diffusion + Gemma + video VAE + audio VAE + connectors)...');
    // no clipVisionModelSrc — LTX-2 img2vid does not use CLIP vision.
    const modelId = await loadModel({
        modelSrc: diffusionModelSrc,
        modelType: 'sdcpp-generation',
        modelConfig: {
            mode: 'video',
            device: 'gpu',
            threads: 4,
            // Supplying embeddingsConnectorsModelSrc selects the LTX-2 layout.
            llmModelSrc,
            vaeModelSrc,
            audioVaeModelSrc,
            embeddingsConnectorsModelSrc,
            diffusion_fa: true,
            vae_tiling: true
        },
        onProgress: (p) => {
            const mb = (n) => (n / 1e6).toFixed(1);
            const line = `▸ Downloading ${p.percentage.toFixed(0)}% (${mb(p.downloaded)}/${mb(p.total)} MB)`;
            process.stderr.write(process.stderr.isTTY ? `\r${line}` : `${line}\n`);
            if (p.percentage >= 100)
                process.stderr.write('\n');
        }
    });
    console.log(`▸ Model loaded: ${modelId}`);
    const init_image = new Uint8Array(fs.readFileSync(initImagePath));
    console.log(`▸ Generating video for: "${prompt}"`);
    const { progressStream, outputs, stats } = video({
        modelId,
        mode: 'img2vid',
        prompt,
        init_image,
        negative_prompt: 'blurry, distorted, low quality, jittery, static, frozen, distorted audio',
        strength: 0.85,
        // LTX-2 requires width/height as multiples of 32 and (8*k + 1) frames (max 257).
        width: 512,
        height: 320,
        video_frames: 121,
        fps: 24,
        steps: 10,
        cfg_scale: 1.0,
        seed: 42,
        temporal_tiling: true
    });
    for await (const { step, totalSteps } of progressStream) {
        console.log(`▸ step ${step}/${totalSteps}`);
    }
    const buffers = await outputs;
    for (let i = 0; i < buffers.length; i++) {
        const outputPath = path.join(outputDir, `ltx_i2v_${i}.avi`);
        fs.writeFileSync(outputPath, buffers[i]);
        console.log(`▸ Saved ${outputPath} (play in VLC to hear the muxed audio track)`);
    }
    console.log('▸ Stats:', await stats);
    await unloadModel({ modelId, clearStorage: false });
    console.log('▸ Done');
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Reference-conditioned text-to-video (LTX-2 IC-LoRA)

The following script conditions LTX-2 on a composite reference sheet so the generated video inherits a specific character or style. Load LTX-2 with lora_apply_mode: 'at_runtime', then pass the reference sheet in reference_images alongside an Ingredients LoRA in lora. Reference conditioning requires at least 121 frames, so video_frames is set to 217:

diffusion-ic-lora-ltx.js
import { GEMMA_3_12B_Q4_K_XL, loadModel, LTX_2_3_22B_DISTILLED_EMBEDDINGS_CONNECTORS, LTX_2_3_22B_Q5_K_M, LTX_2_3_AUDIO_VAE, LTX_2_3_VIDEO_VAE, unloadModel, video } from '@qvac/sdk';
import fs from 'fs';
import path from 'path';
const loraPath = process.argv[2];
const referencePath = process.argv[3];
const prompt = process.argv[4] ||
    'Reference sheet: a red-haired explorer in a blue parka. Generated video: the explorer crosses a snowy ridge at sunrise.';
const outputDir = process.argv[5] || '.';
if (!loraPath || !referencePath) {
    console.error('Usage: bare:example examples/diffusion-ic-lora-ltx.ts <absolute-lora-path> <reference-sheet-path> [prompt] [output-dir]');
    process.exit(1);
}
if (!path.isAbsolute(loraPath)) {
    console.error('The LoRA adapter path must be absolute on the inference worker.');
    process.exit(1);
}
const referenceSheet = fs.readFileSync(referencePath);
try {
    const modelId = await loadModel({
        modelSrc: LTX_2_3_22B_Q5_K_M,
        modelType: 'sdcpp-generation',
        modelConfig: {
            mode: 'video',
            device: 'gpu',
            threads: 4,
            llmModelSrc: GEMMA_3_12B_Q4_K_XL,
            vaeModelSrc: LTX_2_3_VIDEO_VAE,
            audioVaeModelSrc: LTX_2_3_AUDIO_VAE,
            embeddingsConnectorsModelSrc: LTX_2_3_22B_DISTILLED_EMBEDDINGS_CONNECTORS,
            diffusion_fa: true,
            offload_to_cpu: true,
            vae_tiling: true,
            lora_apply_mode: 'at_runtime'
        }
    });
    const { progressStream, outputs, stats } = video({
        modelId,
        mode: 'txt2vid',
        prompt,
        negative_prompt: 'blurry, low quality, static, jittery, watermark',
        width: 768,
        height: 448,
        video_frames: 217,
        fps: 24,
        steps: 8,
        scheduler: 'ltx2',
        cfg_scale: 1,
        seed: 84,
        lora: loraPath,
        lora_strength: 1.37,
        stg_scale: 1,
        stg_block: 29,
        reference_images: [referenceSheet],
        reference_attention_strength: 1,
        reference_downscale_factor: 1,
        temporal_tiling: true,
        vae_tile_size: 4
    });
    for await (const { step, totalSteps } of progressStream) {
        console.log(`▸ step ${step}/${totalSteps}`);
    }
    fs.mkdirSync(outputDir, { recursive: true });
    const buffers = await outputs;
    for (let i = 0; i < buffers.length; i++) {
        const outputPath = path.join(outputDir, `ltx_ic_lora_${i}.avi`);
        fs.writeFileSync(outputPath, buffers[i]);
        console.log(`▸ Saved ${outputPath}`);
    }
    console.log('▸ Stats:', await stats);
    await unloadModel({ modelId, clearStorage: false });
    process.exit(0);
}
catch (error) {
    console.error('✖', error);
    process.exit(1);
}

Text-to-video with audio (MiniMax-H3)

The following script loads MiniMax-H3 with its split layout (diffusion model + Qwen3-VL text encoder + video VAE + audio VAE) and generates a clip with a synchronized 32 kHz audio track. It uses the H3 constraints (dimensions in multiples of 32, 17*k + 5 frames, 24 FPS) and leaves offload_to_cpu on so the run fits on a single GPU:

diffusion-txt2vid-minimax-h3.js
import { loadModel, unloadModel, video } from '@qvac/sdk';
import fs from 'fs';
import path from 'path';
// Pass a directory containing the four MiniMax-H3 files, followed by an output path.
const modelsDir = process.argv[2];
if (!modelsDir) {
    throw new Error('Usage: diffusion-txt2vid-minimax-h3.ts <models-directory> [output.avi]');
}
const outputPath = process.argv[3] || 'minimax-h3.avi';
const modelSrc = path.resolve(modelsDir, process.env['H3_MODEL'] || 'minimax_h3_fl2va_pruned-Q4_K.gguf');
const llmModelSrc = path.resolve(modelsDir, process.env['H3_LLM'] || 'qwen3vl_32b_minimax_h3-Q4_K_M.gguf');
const vaeModelSrc = path.resolve(modelsDir, 'vae/minimax_h3_video_vae_fp16.safetensors');
const audioVaeModelSrc = path.resolve(modelsDir, 'vae/minimax_h3_audio_vae_fp32.safetensors');
for (const file of [modelSrc, llmModelSrc, vaeModelSrc, audioVaeModelSrc])
    fs.accessSync(file);
let modelId;
try {
    modelId = await loadModel({
        modelType: 'sdcpp-generation',
        modelSrc,
        modelConfig: {
            mode: 'video',
            llmModelSrc,
            vaeModelSrc,
            audioVaeModelSrc,
            device: 'gpu',
            diffusion_fa: true,
            offload_to_cpu: process.env['H3_OFFLOAD_TO_CPU'] !== '0',
            stream_layers: process.env['H3_STREAM_LAYERS'] === '1',
            ...(process.env['H3_BACKEND'] && { backend: process.env['H3_BACKEND'] }),
            ...(process.env['H3_PARAMS_BACKEND'] && { params_backend: process.env['H3_PARAMS_BACKEND'] }),
            ...(process.env['H3_MAX_VRAM'] && { max_vram: process.env['H3_MAX_VRAM'] })
        }
    });
    const { progressStream, outputs, stats } = video({
        modelId,
        mode: 'txt2vid',
        prompt: process.env['PROMPT'] ||
            'Premium cinematic coffee commercial. A confident adult sits at a small café table at sunrise, slowly lifts one matte black coffee cup, takes a relaxed sip, and smiles. Warm golden rim light, drifting steam, realistic skin, natural hands, shallow depth of field, subtle slow camera push-in, restrained natural motion, polished live-action advertising, no dialogue, no text overlay.',
        negative_prompt: 'extra people, duplicate cup, malformed hands, cup fused to hand, text, subtitles, watermark, cartoon, CGI, blur, flicker, jitter, camera shake',
        width: 960,
        height: 544,
        // H3 uses 17*k+5 frames; 124 frames at 24 FPS is approximately 5.17 seconds.
        video_frames: Number(process.env['FRAMES'] || 124),
        fps: 24,
        steps: 8,
        cfg_scale: 1,
        seed: 11
    });
    const progress = (async () => {
        for await (const tick of progressStream)
            console.log(`Step ${tick.step}/${tick.totalSteps}`);
    })();
    const [buffers, runtimeStats] = await Promise.all([outputs, stats, progress]);
    if (!buffers[0])
        throw new Error('Generation finished without an AVI result');
    fs.writeFileSync(outputPath, buffers[0]);
    console.log(`Saved ${outputPath}`, runtimeStats);
}
finally {
    if (modelId)
        await unloadModel({ modelId, clearStorage: false });
}
process.exit(0);

The Python client supports this capability through the same worker. A dedicated Python example is not yet published — see the Python SDK for the API surface.

Tip: all examples throughout this documentation are self-contained and runnable. For instructions on how to run them, see the JS/TS quickstart or the Python quickstart.

On this page

Ask anything about QVAC.