World simulation
Turn a single image and navigation controls into a streamed, navigable video using an interactive world model.
Overview
World simulation runs on a customized Diffusion engine (qvac-ext-stable-diffusion.cpp) exposed as an interactive session powered by ABot-World. Load a supported model using modelType: "sdcpp-generation" with modelConfig.mode: "world". Then call worldCreateScene() once to build a world from a text prompt and a first-frame image, and worldStep() repeatedly to walk it. Each call generates one block of video streamed frame-by-frame.
The session runs on the machine hosting the worker; there is no remote route.
World simulation is hardware-intensive. The default 832×480 tier needs ≥20 GB of video memory (a 24 GB card in practice). Pass 448×256 to use the low-VRAM tier, which runs on ~6 GB cards well below interactive frame rates.
Functions
Use the following sequence of function calls:
loadModel()worldCreateScene()— once per worldworldStep()— repeatedly, one call per generated blockunloadModel()
For how to use each function, see SDK — API reference.
worldCreateScene()
Builds the world from a text prompt and a first-frame image (Uint8Array of PNG or JPEG bytes, ≤ 3 MB). Optional width and height are positive multiples of 32, at most 4096 each, and width × height must stay within 2,088,960 pixels (1920×1088). They default to 832×480.
It returns { requestId, stats }, plus scene (a Promise<Uint8Array> holding the scene pack) when you pass returnPack: true. Save that pack and load it back later with modelConfig.sceneSrc to walk the same world again without regenerating it. Calling worldCreateScene() twice on the same session replaces the world and restarts the walk.
worldStep()
Generates the next block of the walk under the keys held for it. One call produces one block — a second call while one is in flight is rejected, so drive the next call off the previous one.
keys accepts three equivalent forms:
- an array of key names (
["W", "L"]), - a key-state object (
{ W: true, L: true }), or - a raw 8-bit mask.
Use WASD to move (forward, left, back, right) and IJKL to steer the camera (up, left, down, right). Pass an empty value for an idle block.
It returns { requestId, frameStream, progressStream, frames, stats }:
frameStream— an async iterable of decoded frames, PNG by default or JPEG whenmodelConfig.world.frameJpegQualityis set. Display each frame as it arrives.progressStream— emits one summary tick per block, after the frames.frames— aPromiseresolving to the whole block once decoding is done.stats— timing and frame counts.
Cancelling with cancel({ requestId }) is block-granular: the current block finishes internally, delivery stops, and the call rejects with InferenceCancelledError. A cancel that lands after the block already completed resolves normally — handle both outcomes.
Models
ABot-World uses a split layout with four models:
ABOT_WORLD_0_5B_Q8_0— the world-model transformer (DiT). Passed asmodelSrc.ABOT_WORLD_0_5B_LF_VAE— the taehv streaming decoder used on every step. Passed asmodelConfig.taehvModelSrc.ABOT_WORLD_0_5B_LF_VAE_F16— the full-precision VAE that encodes the first frame duringworldCreateScene. Passed asmodelConfig.vaeModelSrc.UMT5_XXL_ENC_Q8_0— the UMT5 text encoder for the prompt. Passed asmodelConfig.t5XxlModelSrc.
import {
ABOT_WORLD_0_5B_Q8_0,
ABOT_WORLD_0_5B_LF_VAE,
ABOT_WORLD_0_5B_LF_VAE_F16,
UMT5_XXL_ENC_Q8_0,
loadModel,
} from "@qvac/sdk";
const modelId = await loadModel({
modelSrc: ABOT_WORLD_0_5B_Q8_0,
modelType: "sdcpp-generation",
modelConfig: {
mode: "world",
taehvModelSrc: ABOT_WORLD_0_5B_LF_VAE,
t5XxlModelSrc: UMT5_XXL_ENC_Q8_0,
vaeModelSrc: ABOT_WORLD_0_5B_LF_VAE_F16,
world: { kvCache: true, frameJpegQuality: 85 },
},
});Set world.numFramePerBlock to control the block size; the SDK caps it at 64.
For models available as constants, see SDK — Models.
Example
The following script loads an ABot-World session, creates a world from a first-frame image, walks it for several blocks under a scripted key tape, and writes each decoded frame to disk:
import { cancel, loadModel, unloadModel, worldCreateScene, worldStep, ABOT_WORLD_0_5B_Q8_0, ABOT_WORLD_0_5B_LF_TAEHV_VAE, ABOT_WORLD_0_5B_LF_WAN_VAE, UMT5_XXL_ENC_Q8_0 } from '@qvac/sdk';
import fs from 'fs';
import path from 'path';
// ABot-World: build a world from a single image, then walk it. Each step
// generates one block of video under the keys held for it and streams the
// decoded frames.
//
// Runs on the machine hosting the worker; there is no remote route. The
// default 832x480 tier needs >= 20 GB free VRAM on a dedicated GPU (24 GB card
// in practice); pass 448 256 to use the low-VRAM tier, which runs on ~6 GB
// cards well below interactive frame rates.
const firstFramePath = process.argv[2];
const prompt = process.argv[3] || '| unknown | A realistic outdoor world scene with a navigable path.';
const outputDir = process.argv[4] || '.';
const blocks = Number(process.argv[5] || 8);
const width = Number(process.argv[6] || 832);
const height = Number(process.argv[7] || 480);
if (!firstFramePath) {
console.error('✖ first frame image path is required');
console.error('Usage: bun run bare:example dist/examples/abot-world.js ' +
'<firstFrameImage> [prompt] [outputDir] [blocks] [width] [height]');
process.exit(1);
}
// A scripted walk: forward, forward while looking right, forward, idle. `keys`
// also accepts a key-state object ({ W: true }) or a raw 8-bit mask, so a live
// keyboard handler can pass its state straight through.
const TAPE = [['W'], ['W', 'L'], ['W'], []];
try {
console.log('▸ Loading ABot-World session (DiT + taehv + scene encoders)...');
const modelId = await loadModel({
modelSrc: ABOT_WORLD_0_5B_Q8_0,
modelType: 'sdcpp-generation',
modelConfig: {
mode: 'world',
// Two different VAEs, and the names are close enough to swap by accident:
// taehvModelSrc — taew2_2, the tiny STREAMING decoder. Turns each block's
// latents into pixels during the walk. Used by every worldStep.
// vaeModelSrc — the full-precision Wan2.2 VAE. ENCODES the first frame
// during worldCreateScene, and is not touched again after that.
// Swapping them loads at first and fails once the walk starts.
taehvModelSrc: ABOT_WORLD_0_5B_LF_TAEHV_VAE,
t5XxlModelSrc: UMT5_XXL_ENC_Q8_0,
vaeModelSrc: ABOT_WORLD_0_5B_LF_WAN_VAE,
world: {
seed: 42,
// ~3.7x fewer frame-passes per block. Without it, block times ramp from
// ~1.8 s to ~7.5 s as the recompute window fills.
kvCache: true,
// JPEG rather than PNG: a block is ~14 MB of raw pixels, and this
// example writes every frame to disk.
frameJpegQuality: 85
}
}
});
console.log('▸ Creating the world (umT5 prompt encode + Wan2.2 VAE first-frame encode)...');
const creation = worldCreateScene({
modelId,
prompt,
image: new Uint8Array(fs.readFileSync(firstFramePath)),
width,
height,
// The world is live on the session without this; ask for the bytes only
// because this example saves them to walk the same world again later.
returnPack: true
});
const scene = await creation.scene;
const sceneStats = await creation.stats;
const scenePath = path.join(outputDir, 'world.safetensors');
fs.writeFileSync(scenePath, scene);
console.log(` scene pack written to ${scenePath} (${scene.length} bytes, ${sceneStats?.sceneCreateMs ?? '?'} ms)`);
console.log(' reuse it later with modelConfig.sceneSrc to walk this same world again');
console.log(`▸ Walking ${blocks} blocks...`);
let frameNumber = 0;
for (let block = 0; block < blocks; block++) {
const keys = TAPE[block % TAPE.length];
// One block at a time: a second step while this one streams is rejected,
// so drive the next call off this one rather than firing them in parallel.
const run = worldStep({ modelId, keys });
for await (const frame of run.frameStream) {
const framePath = path.join(outputDir, `frame-${String(frameNumber++).padStart(4, '0')}.jpg`);
fs.writeFileSync(framePath, frame);
}
const stats = await run.stats;
console.log(` block ${block + 1}/${blocks} [${keys.join('+') || 'idle'}] — ` +
`${stats?.frames ?? '?'} frames in ${stats?.stepMs ?? '?'} ms`);
}
console.log(`✔ Wrote ${frameNumber} frames to ${outputDir}`);
// Targeted cancellation. Cancel is block-granular: the engine has no
// mid-block abort, so the current block finishes internally, delivery stops,
// and the step rejects rather than returning a truncated block. A cancel that
// lands after the block already finished legitimately succeeds instead — both
// outcomes are correct, so handle each.
console.log('▸ Cancelling a block mid-flight to show the contract...');
const cancelling = worldStep({ modelId, keys: ['W'] });
// The timer can fire after the block already finished, and cancelling a
// request that is no longer in flight rejects. Handle it here rather than
// leaving a floating promise: an unhandled rejection from a fire-and-forget
// cancel would take the whole example down for the race it is demonstrating.
const cancelTimer = setTimeout(() => {
cancel({ requestId: cancelling.requestId }).catch(() => {
// The block won the race; the await below reports that outcome.
});
}, 200);
try {
await cancelling.frames;
console.log(' the block completed before the cancel landed (the tolerated race)');
}
catch (error) {
console.log(` cancelled as expected: ${error instanceof Error ? error.message : String(error)}`);
}
finally {
clearTimeout(cancelTimer);
}
// A cancelled step is terminal for the native session, but the SDK drops it
// for you: another worldStep on this same modelId would transparently rebuild
// from the promoted pack, with no unloadModel/loadModel cycle. Unloading here
// only because the example is finished.
await unloadModel({ modelId });
}
catch (error) {
console.error('✖ World walk failed:', error instanceof Error ? error.message : error);
process.exit(1);
}
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.