/** * video_rvq.ts — VideoRVQCodec: the pixels ⇄ tokens bottleneck that lets Evermind * generate video without changing the generator. * * Evermind is a discrete-token autoregressive SSM. To make it a video model you * do NOT touch the model — you give it a codec that maps a clip to a stream of * discrete tokens and back. This is the visual analogue of the BPE tokenizer: * where BPE turns text ⇄ ids, VideoRVQCodec turns frames ⇄ ids. * * Design — TEMPORAL residual vector quantization (the "video-from-the-start" * choice): * * • Each frame is cut into non-overlapping p×p patches; each patch is a vector * of length p·p·C (the "latent"). * • KEYFRAMES (frame 0 and every `keyframeInterval`-th frame) are quantized * directly against the INTRA codebook bank. * • Every other frame is INTER: we quantize the *residual against the previous * RECONSTRUCTED frame* against the INTER bank. Video is mostly temporally * redundant, so these deltas are small and cheap — this is exactly why an * SSM (linear-time in sequence length) is the right generator for the long * token streams video produces. * • Quantization at each bank is RESIDUAL VQ over `levels` codebooks: level 0 * picks the nearest entry, level 1 quantizes what level 0 missed, and so on. * More levels ⇒ finer reconstruction ⇒ more tokens per patch. * * The encoder runs closed-loop (it references its own reconstruction, never the * ground-truth previous frame) so the decoder — which only ever has * reconstructions — stays exactly in sync. * * Codebooks start random (a cold codec is lossy, like any untrained neural * codec) and are learned by {@link VideoRVQCodec.fit} (greedy per-level k-means). * Reaching photoreal fidelity needs training on a real corpus with real compute * — that is the one genuine blocker on shipping generated video, not the wiring. * * Pure CPU, zero deps, deterministic under a seed — same conventions as the rest * of the engine. A WGSL/WebGPU acceleration is a future drop-in with these shapes. */ import { SeededRng } from "../utils/rng.js"; import { MultimodalVocab, VIDEO_BANK_INTRA, VIDEO_BANK_INTER } from "./multimodal_vocab.js"; /** A single frame: length `height·width·channels`, layout `((y·W)+x)·C + ch`, values in [0,1]. */ export type Frame = Float32Array; /** A clip: T frames, all the same shape. */ export type Video = Frame[]; export interface VideoRVQConfig { height: number; width: number; /** Colour channels. Default 3 (RGB). */ channels?: number; /** Square patch size; `height` and `width` must be divisible by it. Default 4. */ patch?: number; /** Residual-VQ depth (codes per patch). Default 2. */ levels?: number; /** Codebook entries per level per bank. Default 16. */ codebookSize?: number; /** Emit a keyframe every N frames (frame 0 is always a keyframe). Default 12. */ keyframeInterval?: number; /** Size of the text region this codec's tokens sit above; 0 ⇒ pure-video vocab. Default 0. */ textVocabSize?: number; /** Deterministic seed for codebook init. */ seed?: number; } const VRQ_MAGIC = 0x56525130; // "VRQ0" const DEFAULTS = { channels: 3, patch: 4, levels: 2, codebookSize: 16, keyframeInterval: 12, textVocabSize: 0, seed: 0x56524651, // "VRFQ" } as const; export class VideoRVQCodec { readonly height: number; readonly width: number; readonly channels: number; readonly patch: number; readonly levels: number; readonly codebookSize: number; readonly keyframeInterval: number; /** Patch latent dimension = patch·patch·channels. */ readonly latentDim: number; /** Patches per frame = (H/patch)·(W/patch). */ readonly patchesPerFrame: number; /** Code tokens emitted per frame = patchesPerFrame·levels. */ readonly tokensPerFrame: number; /** The unified text+video vocabulary; feed `.vocab.size` to EvermindLM. */ readonly vocab: MultimodalVocab; /** Two banks × levels codebooks; each codebook is `codebookSize × latentDim` row-major. */ private readonly banks: [Float32Array[], Float32Array[]]; constructor(config: VideoRVQConfig) { const channels = config.channels ?? DEFAULTS.channels; const patch = config.patch ?? DEFAULTS.patch; if (config.height <= 0 || config.width <= 0) throw new Error("VideoRVQCodec: height/width must be > 0"); if (config.height % patch !== 0 || config.width % patch !== 0) { throw new Error(`VideoRVQCodec: height/width must be divisible by patch (${patch})`); } this.height = config.height; this.width = config.width; this.channels = channels; this.patch = patch; this.levels = config.levels ?? DEFAULTS.levels; this.codebookSize = config.codebookSize ?? DEFAULTS.codebookSize; this.keyframeInterval = Math.max(1, config.keyframeInterval ?? DEFAULTS.keyframeInterval); this.latentDim = patch * patch * channels; this.patchesPerFrame = (this.height / patch) * (this.width / patch); this.tokensPerFrame = this.patchesPerFrame * this.levels; this.vocab = new MultimodalVocab({ textVocabSize: config.textVocabSize ?? DEFAULTS.textVocabSize, levels: this.levels, codebookSize: this.codebookSize, }); const rng = new SeededRng((config.seed ?? DEFAULTS.seed) >>> 0 || 1); const mkBank = (): Float32Array[] => Array.from({ length: this.levels }, () => { const cb = new Float32Array(this.codebookSize * this.latentDim); // Small-magnitude init: intra entries hover near mid-grey, inter (delta) near 0. for (let i = 0; i < cb.length; i++) cb[i] = (rng.next() - 0.5) * 0.1; return cb; }); this.banks = [mkBank(), mkBank()]; // Bias the intra bank toward the [0,1] pixel range so a cold codec is not black. for (const cb of this.banks[VIDEO_BANK_INTRA]) for (let i = 0; i < cb.length; i++) cb[i] += 0.5; } /** Total vocabulary size for this codec's model. */ get vocabSize(): number { return this.vocab.size; } // ── Serialize (so a trained codec ships alongside the model artifact) ─────────── /** * Serialize config + learned codebooks to a compact "VRQ0" binary. A generated * video model is only servable if its codec travels with it (the decoder needs * these codebooks), so this is the video analogue of EvermindLM.exportWeights. */ serialize(): ArrayBuffer { const headerEls = 9; // magic, h, w, channels, patch, levels, codebookSize, keyframeInterval, textVocabSize const total = 2 * this.levels * this.codebookSize * this.latentDim; const buf = new ArrayBuffer(headerEls * 4 + total * 4); const head = new Uint32Array(buf, 0, headerEls); head[0] = VRQ_MAGIC; head[1] = this.height; head[2] = this.width; head[3] = this.channels; head[4] = this.patch; head[5] = this.levels; head[6] = this.codebookSize; head[7] = this.keyframeInterval; head[8] = this.vocab.textVocabSize; const body = new Float32Array(buf, headerEls * 4, total); let o = 0; for (const bank of this.banks) { for (const cb of bank) { body.set(cb, o); o += cb.length; } } return buf; } /** Reconstruct a codec (config + codebooks) from a "VRQ0" binary. */ static deserialize(buffer: ArrayBuffer): VideoRVQCodec { const head = new Uint32Array(buffer, 0, 9); if (head[0] !== VRQ_MAGIC) throw new Error("VideoRVQCodec.deserialize: bad magic (not a VRQ0 blob)"); const codec = new VideoRVQCodec({ height: head[1]!, width: head[2]!, channels: head[3]!, patch: head[4]!, levels: head[5]!, codebookSize: head[6]!, keyframeInterval: head[7]!, textVocabSize: head[8]!, }); const total = 2 * codec.levels * codec.codebookSize * codec.latentDim; const body = new Float32Array(buffer.slice(36, 36 + total * 4)); let o = 0; for (const bank of codec.banks) { for (const cb of bank) { cb.set(body.subarray(o, o + cb.length)); o += cb.length; } } return codec; } // ── Encode ─────────────────────────────────────────────────────────────────── /** Encode a clip to a self-delimiting token stream: ` (marker codes…)… `. */ encode(video: Video): number[] { this.assertShape(video); const out: number[] = [this.vocab.bosVideo]; let prevRecon: Float32Array[] | null = null; for (let t = 0; t < video.length; t++) { const patches = this.toPatches(video[t]!); const isKey = prevRecon === null || t % this.keyframeInterval === 0; const bankId = isKey ? VIDEO_BANK_INTRA : VIDEO_BANK_INTER; out.push(isKey ? this.vocab.frameKey : this.vocab.frameDelta); const reconPatches: Float32Array[] = []; for (let p = 0; p < this.patchesPerFrame; p++) { const target = isKey ? patches[p]! : sub(patches[p]!, prevRecon![p]!); const { codes, recon } = this.rvqEncode(bankId, target); for (let l = 0; l < this.levels; l++) out.push(this.vocab.codeToken(bankId, l, codes[l]!)); reconPatches.push(isKey ? recon : add(prevRecon![p]!, recon)); } prevRecon = reconPatches; } out.push(this.vocab.eosVideo); return out; } // ── Decode ─────────────────────────────────────────────────────────────────── /** * Decode a token stream back to frames. Tolerant by design: it skips text / * stray tokens, treats a frame whose codes run short as zero-padded, and stops * at `` or end-of-stream — so a stream sampled from an under-trained * generator still yields a valid (if noisy) clip instead of throwing. */ decode(tokens: number[]): Video { const frames: Video = []; let prevRecon: Float32Array[] | null = null; let i = 0; // Advance to the first frame marker (tolerate a leading prompt / ). while (i < tokens.length && !this.vocab.isVideoMarker(tokens[i]!)) { if (tokens[i] === this.vocab.eosVideo) return frames; i++; } while (i < tokens.length) { const marker = tokens[i]!; if (!this.vocab.isVideoMarker(marker)) break; // , text, or garbage → end i++; const isKey = marker === this.vocab.frameKey || prevRecon === null; const bankId = isKey ? VIDEO_BANK_INTRA : VIDEO_BANK_INTER; // Pull up to tokensPerFrame code tokens; stop early on the next marker/EOS. const codes: number[] = []; while (codes.length < this.tokensPerFrame && i < tokens.length && this.vocab.isCode(tokens[i]!)) { codes.push(tokens[i]!); i++; } const reconPatches: Float32Array[] = []; for (let p = 0; p < this.patchesPerFrame; p++) { const recon = new Float32Array(this.latentDim); for (let l = 0; l < this.levels; l++) { const slot = p * this.levels + l; const tok = slot < codes.length ? codes[slot]! : -1; if (tok < 0) continue; // missing → contribute zero (graceful degradation) const { level, code } = this.vocab.decodeCode(tok); addRowInto(recon, this.banks[bankId]![level]!, code, this.latentDim); } reconPatches.push(isKey ? recon : add(prevRecon![p]!, recon)); } frames.push(this.fromPatches(reconPatches)); prevRecon = reconPatches; } return frames; } // ── Learn codebooks ──────────────────────────────────────────────────────────── /** * Learn both codebook banks from a set of clips (greedy per-level k-means over * residuals — the standard way to train residual VQ). Returns the mean * reconstruction MSE over the training clips after fitting. This is the codec's * "training"; the generator is trained separately on the resulting token streams. */ fit(videos: Video[], opts: { iterations?: number; seed?: number } = {}): number { const iterations = opts.iterations ?? 8; const rng = new SeededRng((opts.seed ?? 0x66697421) >>> 0 || 1); // "fit!" for (const v of videos) this.assertShape(v); // Gather intra patches and inter deltas (open-loop deltas are a fine proxy for fitting). const intra: Float32Array[] = []; const inter: Float32Array[] = []; for (const video of videos) { let prev: Float32Array[] | null = null; for (let t = 0; t < video.length; t++) { const patches = this.toPatches(video[t]!); const isKey = prev === null || t % this.keyframeInterval === 0; if (isKey) for (const p of patches) intra.push(p); else for (let p = 0; p < patches.length; p++) inter.push(sub(patches[p]!, prev![p]!)); prev = patches; } } this.fitBank(VIDEO_BANK_INTRA, intra, iterations, rng); this.fitBank(VIDEO_BANK_INTER, inter, iterations, rng); // Report closed-loop reconstruction error over the corpus. let se = 0; let n = 0; for (const video of videos) { const recon = this.decode(this.encode(video)); for (let t = 0; t < video.length; t++) { const a = video[t]!; const b = recon[t]!; for (let k = 0; k < a.length; k++) { const d = a[k]! - b[k]!; se += d * d; } n += a.length; } } return n > 0 ? se / n : 0; } /** Fit one bank's `levels` codebooks greedily over residuals. */ private fitBank(bankId: number, vectors: Float32Array[], iterations: number, rng: SeededRng): void { if (vectors.length === 0) return; let residuals = vectors.map((v) => Float32Array.from(v)); for (let l = 0; l < this.levels; l++) { const centroids = kmeans(residuals, this.codebookSize, this.latentDim, iterations, rng); this.banks[bankId]![l] = centroids; residuals = residuals.map((r) => { const idx = nearestRow(centroids, r, this.codebookSize, this.latentDim); const out = Float32Array.from(r); const off = idx * this.latentDim; for (let d = 0; d < this.latentDim; d++) out[d] = out[d]! - centroids[off + d]!; return out; }); } } // ── Residual VQ over one bank ─────────────────────────────────────────────────── private rvqEncode(bankId: number, vector: Float32Array): { codes: number[]; recon: Float32Array } { const residual = Float32Array.from(vector); const recon = new Float32Array(this.latentDim); const codes: number[] = []; for (let l = 0; l < this.levels; l++) { const cb = this.banks[bankId]![l]!; const idx = nearestRow(cb, residual, this.codebookSize, this.latentDim); codes.push(idx); const off = idx * this.latentDim; for (let d = 0; d < this.latentDim; d++) { recon[d] = recon[d]! + cb[off + d]!; residual[d] = residual[d]! - cb[off + d]!; } } return { codes, recon }; } // ── Patch <-> frame ───────────────────────────────────────────────────────────── private toPatches(frame: Frame): Float32Array[] { const { patch, channels, width } = this; const pw = this.width / patch; const ph = this.height / patch; const out: Float32Array[] = []; for (let py = 0; py < ph; py++) { for (let px = 0; px < pw; px++) { const vec = new Float32Array(this.latentDim); for (let dy = 0; dy < patch; dy++) { for (let dx = 0; dx < patch; dx++) { const y = py * patch + dy; const x = px * patch + dx; const src = ((y * width) + x) * channels; const dst = ((dy * patch) + dx) * channels; for (let c = 0; c < channels; c++) vec[dst + c] = frame[src + c]!; } } out.push(vec); } } return out; } private fromPatches(patches: Float32Array[]): Frame { const { patch, channels, width } = this; const pw = this.width / patch; const frame = new Float32Array(this.height * this.width * channels); for (let p = 0; p < patches.length; p++) { const py = Math.floor(p / pw); const px = p - py * pw; const vec = patches[p]!; for (let dy = 0; dy < patch; dy++) { for (let dx = 0; dx < patch; dx++) { const y = py * patch + dy; const x = px * patch + dx; const dst = ((y * width) + x) * channels; const src = ((dy * patch) + dx) * channels; for (let c = 0; c < channels; c++) frame[dst + c] = clamp01(vec[src + c]!); } } } return frame; } private assertShape(video: Video): void { const expect = this.height * this.width * this.channels; for (let t = 0; t < video.length; t++) { if (video[t]!.length !== expect) { throw new Error(`VideoRVQCodec: frame ${t} has length ${video[t]!.length}, expected ${expect}`); } } } } // ── free helpers ───────────────────────────────────────────────────────────────── function sub(a: Float32Array, b: Float32Array): Float32Array { const out = new Float32Array(a.length); for (let i = 0; i < a.length; i++) out[i] = a[i]! - b[i]!; return out; } function add(a: Float32Array, b: Float32Array): Float32Array { const out = new Float32Array(a.length); for (let i = 0; i < a.length; i++) out[i] = a[i]! + b[i]!; return out; } function addRowInto(acc: Float32Array, codebook: Float32Array, row: number, dim: number): void { const off = row * dim; for (let d = 0; d < dim; d++) acc[d] = acc[d]! + codebook[off + d]!; } function clamp01(v: number): number { return v < 0 ? 0 : v > 1 ? 1 : v; } /** Nearest codebook row to `v` by squared L2. `codebook` is `count × dim` row-major. */ function nearestRow(codebook: Float32Array, v: Float32Array, count: number, dim: number): number { let best = 0; let bestD = Infinity; for (let r = 0; r < count; r++) { const off = r * dim; let d = 0; for (let k = 0; k < dim; k++) { const diff = v[k]! - codebook[off + k]!; d += diff * diff; if (d >= bestD) break; } if (d < bestD) { bestD = d; best = r; } } return best; } /** Lloyd's k-means → `count × dim` centroids. Empty clusters reseed to a random sample. */ function kmeans(vectors: Float32Array[], count: number, dim: number, iterations: number, rng: SeededRng): Float32Array { const centroids = new Float32Array(count * dim); // Init: distinct random samples. for (let r = 0; r < count; r++) { const src = vectors[Math.floor(rng.next() * vectors.length) % vectors.length]!; centroids.set(src, r * dim); } const sums = new Float32Array(count * dim); const counts = new Int32Array(count); for (let it = 0; it < iterations; it++) { sums.fill(0); counts.fill(0); for (const v of vectors) { const r = nearestRow(centroids, v, count, dim); counts[r] = counts[r]! + 1; const off = r * dim; for (let d = 0; d < dim; d++) sums[off + d] = sums[off + d]! + v[d]!; } for (let r = 0; r < count; r++) { const off = r * dim; if (counts[r]! === 0) { // Reseed the dead centroid onto a random sample so it can win points next round. const src = vectors[Math.floor(rng.next() * vectors.length) % vectors.length]!; centroids.set(src, off); continue; } const inv = 1 / counts[r]!; for (let d = 0; d < dim; d++) centroids[off + d] = sums[off + d]! * inv; } } return centroids; }