Everything to know about generating video from a text prompt: the 3-D VAE + spatio-temporal DiT architecture that closed the gap with image generation, the mid-2026 model landscape, why VBench numbers lie, and runnable code for the open models that actually fit on a 12 GB card.
Author
Benedict Thekkel
1. What is Text-to-Video?
Text-to-video (T2V) maps a natural-language prompt to a short video clip: no reference image, no reference video, nothing but the string.
Input. A prompt, encoded by a frozen text encoder. Which encoder is a real architectural decision (section 3): the 2023 generation used CLIP’s 77-token text tower (123M params); every 2024+ model uses a T5-XXL / UMT5-XXL encoder (~4.7B params) instead, and that single choice is the biggest lever on prompt adherence - and, as you will see in section 7, the single biggest consumer of memory on a small box.
Output. A tensor of shape (T, H, W, 3) - typically 5 s at 16-24 fps, 480p-720p for open models. Everything downstream (a GIF, an MP4, a contact sheet) is just a view of that tensor. Length is the hard part: open models generate a fixed window (49, 81 or 121 frames) in one shot, and “make it longer” is a separate research problem, not a parameter.
What it is not. T2V is unconditioned on pixels, so it must invent the subject, the scene, the camera and the motion simultaneously. That is why the sibling task 07_Image_to_Video is easier and more controllable: an input frame pins down identity, composition, lighting and style for free, leaving the model only the motion to solve. In production, “text-to-video” pipelines are very often text-to-image-to-video for exactly that reason.
Neighbouring tasks:
Task
What it does
Notebook / typical tool
Text-to-image
The static ancestor; diffusion, CFG, samplers, LoRA all come from here
see 04_Text_to_Image
Image-to-video
Animate a given first frame (and optionally a last frame)
see 07_Image_to_Video
Video-to-video
Restyle / edit / upscale an existing clip; ControlNet-style conditioning
see 18_Video_to_Video
Video classification
The discriminative inverse: video in, label out
see 09_Video_Classification
Video captioning
Video in, text out - and the labeling engine behind every T2V training set
see 05_Image_to_Text (image case)
Text-to-audio
The missing half of most open video models (see section 15)
see Audio/01_Text_to_Audio
This notebook assumes the diffusion fundamentals from 04_Text_to_Image - forward/reverse process, classifier-free guidance, samplers, the latent-space trick. They are not re-derived here. What is new in video is the temporal axis, and that is what section 3 is about.
Product brief + brand prompt -> dozens of 5-10 s cuts
Cost per clip and throughput; brand-safe consistency across variants matters more than fidelity
Storyboard / previz
Film and TV pre-production
Script beat -> rough moving shot
Camera controllability and iteration speed; final quality is irrelevant at this stage
Stock-footage replacement
Media, e-learning, corporate video
“Drone shot over autumn forest” -> 4K b-roll
Resolution and licence cleanliness (can the output be sold?)
Game and world prototyping
Games (Genie-style world models, Decart)
Prompt -> playable/streamable frames
Latency: an interactive world needs causal, streaming generation at 15+ fps, which rules out bidirectional diffusion entirely
Synthetic training data
Robotics / AV
Prompt or scene spec -> rare-event clips
Physical plausibility; a video that violates dynamics poisons the downstream policy
Product and UGC video
E-commerce (Amazon, Shopify, TikTok Symphony)
Product image + prompt -> demo clip
Identity preservation of the product - which is why these are I2V, not T2V, in practice
Accessibility / education
EdTech
Concept description -> explanatory animation
Factual and diagrammatic correctness; text rendered in frame must be legible, and it usually is not
What the leaderboard number hides. The Elo ranking is measured on 5-second, single-shot, cherry-pickable clips - which is roughly the only regime in which these models are good. Three realities dominate any actual deployment:
Cost is not a rounding error. A 5 s 720p clip from a 14B DiT is 5-20 GPU-minutes on a consumer card, seconds-to-a-minute on an H100 cluster. Image generation is ~1 s. You cannot design a product as if video were free, and the entire 2025-2026 research wave (distillation, caching, sparse attention) exists to attack this one number.
Prompt adherence collapses with prompt complexity. Two subjects interacting, a specified camera move, a specified sequence of actions (“she picks up the cup, then drinks”) - the failure rate rises sharply. Production systems hide this by rewriting the prompt with an LLM before it ever reaches the video model (section 15).
Failure modes are semantic, not visual. Modern output is pretty and wrong: objects pass through each other, a glass un-shatters, a hand grows a sixth finger, an occluded object comes back as a different object, and any text in frame is gibberish. Quality metrics score these clips highly. Humans do not.
And the deployment fork mirrors the ASR streaming/batch fork: bidirectional diffusion cannot stream by construction (every frame attends to every other frame, so nothing is emitted until the whole clip is denoised). If your product needs frames as they are generated - interactive worlds, live avatars - you need a causal/autoregressive model, which is a different architecture, not a flag.
3. How Modern Text-to-Video Works
Video generation lagged image generation by roughly two years, and the reason is one number: cost scales with the number of latent tokens, and attention scales with its square. A 720p image is ~4k latent tokens. A 5-second 720p video is, naively, 24x that. Every generation of this architecture is an answer to “how do we not pay 24x quadratically”.
Generation 1 (2022-2023): inflate a T2I UNet with temporal layers
Take a pretrained Stable Diffusion UNet, freeze most of it, and insert temporal attention / 1-D temporal convolutions between the existing spatial blocks. The 2-D VAE still encodes each frame independently - there is no temporal compression at all. Examples: ModelScope T2V (1.7B, the first widely usable open T2V), VideoLDM, AnimateDiff (which factors the temporal layers into a motion adapter module that plugs into any SD 1.5 checkpoint, so the community’s thousands of LoRAs and finetunes keep working).
Pro: cheap, reuses all the image priors, trains on modest data, still the fastest thing you can run.
Con: short (16 frames), low motion, and flickery - because temporal attention is a bolt-on, and the VAE never sees more than one frame at a time. The motion is a slideshow with interpolation, not physics.
Generation 2 (2024): the 3-D causal VAE
The central insight: compress time as well as space. A 3-D causal VAE encodes a (T, H, W) clip into (T/c_t, H/c_s, W/c_s) latents - causal so the first frame can be encoded alone (which is what lets the same VAE serve image-to-video). CogVideoX (2024) uses c_t=4, c_s=8; Wan 2.1 the same; LTX-Video pushes to c_t=8, c_s=32 (a ~1:192 pixel-to-latent ratio); Wan 2.2’s TI2V-5B goes to c_t=4, c_s=16.
This is the memory lever, and you can compute it directly. Latent token count after patchifying with patch size p:
CogVideoX-2B, 49 frames @ 480x720, c=(4,8), p=2: 13 x 30 x 45 = 17,550 tokens
Wan 2.1, 81 frames @ 480x832, c=(4,8), p=2: 21 x 30 x 52 = 32,760 tokens
LTX-Video, 121 frames @ 512x768, c=(8,32), p=1: 16 x 16 x 24 = 6,144 tokens
LTX generates more frames than CogVideoX from one third the tokens. Since attention is O(N^2), that is a ~8x cheaper attention bill - and it is the whole reason LTX-Video is the “real-time on a consumer GPU” model while CogVideoX is not. The price is paid at the VAE: a 1:192 compression ratio throws away high-frequency detail, so LTX’s decoder has to do the last denoising step itself to claw it back.
Generation 3 (2024-2025): spatio-temporal DiT + full 3-D attention + flow matching
OpenAI’s Sora (Feb 2024, “Video generation models as world simulators”) reframed the whole thing: spacetime patches are just tokens, a video is a sequence of them, and a transformer eats sequences of any length. Drop the UNet, use a DiT with full 3-D self-attention over all T x H x W latent tokens jointly (rather than factorised spatial-then-temporal attention, which is cheaper but demonstrably worse at motion), condition on the T5 text embedding via cross-attention or adaLN, and train with flow matching / rectified flow rather than DDPM epsilon-prediction (straighter probability paths, fewer sampling steps, more stable at scale). Meta’s Movie Gen (30B, Oct 2024) and every strong open model since - HunyuanVideo (13B), Wan 2.1/2.2, Mochi 1 (10B AsymmDiT) - follow this recipe.
Full 3-D attention over 30k+ tokens is the cost story. It is why a 14B video DiT is slower per clip than a 70B LLM is per response.
Generation 4 (2025-2026): making it affordable
Scaling stopped being the interesting axis; efficiency took over.
Few-step distillation. Distil a 50-step teacher into a 4-8 step student (LCM/DMD-style, LightX2V LoRAs for Wan, the LTX -distilled checkpoints). 5-10x wall-clock, and the community treats these as the default now.
Causal / autoregressive and streaming models.CausVid (2025) and Self-Forcing (2025) distil a bidirectional teacher into a causal student that generates frame-blocks left-to-right with a KV cache, closing the train/test gap that made earlier autoregressive video models drift. This is what makes real-time, infinite-length, interactive generation possible - and it is a fundamentally different deployment shape (section 2).
MoE video DiTs.Wan 2.2 A14B is the first: two experts, one specialised for the high-noise (layout/motion) phase and one for the low-noise (detail) phase of denoising, 27B total but only ~14B active per step. Denoising is not a homogeneous task, so different timesteps get different weights.
Native audio. Veo 3 (closed) and LTX-2 (open, Jan 2026) generate synchronised audio in the same diffusion pass. This is the clearest current capability gap in open video (section 15).
Cheat sheet
Approach
Example
Latent tokens
Motion quality
Speed
Streams?
Inflated 2-D UNet + temporal layers
AnimateDiff, ModelScope T2V
high (no temporal compression)
weak, flickery
fastest
no
3-D VAE + DiT, moderate compression
CogVideoX, Wan 2.1
~20-35k
good
slow
no
3-D VAE + DiT, aggressive compression
LTX-Video
~6k
good (less fine detail)
fast
no
MoE DiT, full 3-D attention
Wan 2.2 A14B
~35k+
best open
very slow
no
Causal / distilled AR
Self-Forcing, CausVid
n/a (block-wise)
good
real-time
yes
4. Evaluation Metrics
FVD (Frechet Video Distance) - the classic. Embed real and generated clips with a pretrained video network (I3D), fit a Gaussian to each set, and take the Frechet distance:
Its caveats are severe. It needs hundreds of clips per model; it is dominated by the I3D backbone’s biases (it is sensitive to per-frame appearance and surprisingly insensitive to temporal corruption - shuffle the frames and FVD barely moves); it depends on the reference set; and it is not comparable across papers because everyone preprocesses differently. Treat any cross-paper FVD comparison as meaningless.
CLIPSIM / text-video alignment - mean CLIP cosine similarity between the prompt embedding and each frame embedding:
Cheap and useful, but note what it cannot see: CLIP is a per-frame image model, so CLIPSIM is completely blind to motion. A single still image that matches the prompt scores as well as a perfectly animated clip. We demonstrate exactly this below.
VBench / VBench-2.0 is the standard the field actually reports. VBench (CVPR 2024) decomposes quality into 16 automated dimensions in two groups:
VBench-2.0 (2025) adds “intrinsic faithfulness”: Human Fidelity, Controllability, Creativity, Physics, Commonsense, across 18 sub-dimensions (human anatomy, mechanics, thermotics, motion order understanding, multi-view consistency, …) - explicitly targeting the failures listed in section 2.
The key point about VBench: the dimensions trade off against each other, so a single averaged score is meaningless. A model that generates a nearly static clip scores brilliantly on subject consistency, background consistency, motion smoothness and temporal flickering - there is nothing moving to be inconsistent about - while scoring near zero on dynamic degree. Averaging those together rewards the model that does nothing. Always read dynamic degree next to the consistency dimensions; VBench itself warns about this and normalises accordingly.
What actually ranks models: humans. The Artificial Analysis Video Arena runs blind pairwise votes on the same prompt and reports an Elo. It is the only ranking that correlates with what people will pay for, and it is where the closed frontier (Veo, Sora, Kling, Seedance) is visibly ahead of open weights.
And what no metric catches well: physics violations (objects interpenetrating, gravity ignored, the classic glass un-breaking), object permanence across occlusion, in-frame text rendering, and hands / limb counts. These are exactly the things a human notices in the first half-second.
Speed metrics. Report seconds per second-of-video (wall clock / clip duration) and peak VRAM, not “seconds per clip” - clip lengths differ between models and a per-clip number is not comparable.
Below we implement two of the proxies from scratch with CLIP (from transformers), plus a dynamic-degree proxy, and run them on two synthetic clips - a moving square and a static square - to make the trade-off concrete.
import gcimport numpy as npimport torchfrom PIL import Imagefrom transformers import CLIPModel, CLIPProcessordev ="cuda:0"if torch.cuda.is_available() else"cpu"CACHE ="../../datasets/hf_cache"# same gitignored cache the Setup cell formalises as HF_CACHE# --- two synthetic 16-frame clips: a red square that MOVES, and one that does not ---def square_clip(n_frames=16, size=224, move=True):"Render a red square on white; `move` sweeps it left-to-right." frames = []for i inrange(n_frames): canvas = np.full((size, size, 3), 255, dtype=np.uint8) x =int(20+ i * (size -90) / (n_frames -1)) if move else20 canvas[90:150, x:x +60] = [220, 30, 30] frames.append(Image.fromarray(canvas))return framesmoving, static = square_clip(move=True), square_clip(move=False)# --- the metrics ---clip_id ="openai/clip-vit-base-patch32"# 150M - the only "small" model in this notebookclip_model = CLIPModel.from_pretrained(clip_id, cache_dir=CACHE).to(dev).eval()clip_proc = CLIPProcessor.from_pretrained(clip_id, cache_dir=CACHE)@torch.inference_mode()def clip_frame_embeds(frames):"L2-normalised CLIP image embedding per frame -> (T, D)." inp = clip_proc(images=frames, return_tensors="pt").to(dev) e = clip_model.get_image_features(**inp).pooler_outputreturn torch.nn.functional.normalize(e, dim=-1)@torch.inference_mode()def clip_text_embed(prompt): inp = clip_proc(text=[prompt], return_tensors="pt", padding=True, truncation=True).to(dev) e = clip_model.get_text_features(**inp).pooler_outputreturn torch.nn.functional.normalize(e, dim=-1)def text_alignment(frames, prompt):"CLIPSIM: mean cosine(prompt, frame) over frames. Returns (mean, per-frame list)." per_frame = (clip_frame_embeds(frames) @ clip_text_embed(prompt).T).squeeze(-1)returnfloat(per_frame.mean()), [float(x) for x in per_frame.cpu()]def temporal_consistency(frames):"Mean cosine similarity between ADJACENT frame embeddings. Returns (mean, per-pair list)." e = clip_frame_embeds(frames) pairs = (e[:-1] * e[1:]).sum(-1)returnfloat(pairs.mean()), [float(x) for x in pairs.cpu()]def dynamic_degree(frames):"Proxy for VBench's dynamic degree: mean |pixel delta| between adjacent frames, 0-1." a = np.stack([np.asarray(f, dtype=np.float32) for f in frames]) /255.0returnfloat(np.abs(a[1:] - a[:-1]).mean())PROMPT ="a red square moving across a white background"for name, clip in [("moving square", moving), ("static square", static)]: align, _ = text_alignment(clip, PROMPT) cons, _ = temporal_consistency(clip)print(f"{name:14s} align {align:.4f} temporal-consistency {cons:.4f} dynamic {dynamic_degree(clip):.4f}")
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
Read that output carefully - it is the whole argument of this section in three numbers.
The static clip wins temporal consistency outright (adjacent frames are identical, so cosine similarity is exactly 1.000) and its text-alignment score is essentially the same as the moving clip’s, because CLIP scores each frame in isolation and cannot perceive the word “moving”. Only the dynamic-degree proxy separates them. A benchmark that averaged consistency and alignment would declare the still image the better video generator.
The per-frame chart makes the shape of it visible:
from pyecharts import options as optsfrom pyecharts.charts import Line_, mov_align = text_alignment(moving, PROMPT)_, sta_align = text_alignment(static, PROMPT)_, mov_cons = temporal_consistency(moving)_, sta_cons = temporal_consistency(static)line = ( Line() .add_xaxis([str(i) for i inrange(len(mov_align))]) .add_yaxis("moving: text alignment", [round(v, 4) for v in mov_align]) .add_yaxis("static: text alignment", [round(v, 4) for v in sta_align]) .add_yaxis("moving: adjacent-frame consistency", [round(v, 4) for v in mov_cons] + [None]) .add_yaxis("static: adjacent-frame consistency", [round(v, 4) for v in sta_cons] + [None]) .set_series_opts(label_opts=opts.LabelOpts(is_show=False)) .set_global_opts( title_opts=opts.TitleOpts( title="CLIP proxies on a moving vs static square", subtitle="the static clip is 'perfectly consistent' and equally 'aligned' - and is not a video", ), xaxis_opts=opts.AxisOpts(name="frame index"), yaxis_opts=opts.AxisOpts(name="cosine similarity", min_=0, max_=1.05), tooltip_opts=opts.TooltipOpts(trigger="axis"), legend_opts=opts.LegendOpts(pos_top="12%"), ))# Free CLIP before the video models load - section 13 re-creates `clip_model` / `clip_proc`# and reuses the scoring functions above unchanged.del clip_model, clip_procgc.collect()if torch.cuda.is_available(): torch.cuda.empty_cache()line.render_notebook()
5. Datasets
Video-text pairs are the bottleneck of the whole field. Nobody has a LAION-5B for video, captions are machine-generated (a captioning VLM labels scraped clips), and the licence position of every large scrape is uncomfortable.
Dataset
Contents
Size
Scope
License
Typical use
WebVid-10M
Stock-footage clips + alt-text
10M clips
web stock video
taken down
The field’s ImageNet-moment dataset (ModelScope, AnimateDiff, VideoLDM all trained on it), withdrawn in 2024 after a Shutterstock complaint. Cited everywhere; do not build on it.
This notebook downloads the VBench prompt suite (all_dimension.txt) into DL_tasks/datasets/, so you can swap in leaderboard prompts at will; the head-to-head in section 13 runs one fixed prompt with one fixed seed through every model, because on this hardware a single clip per model is already 20+ minutes of compute. Nothing here is gated. We deliberately do not compute FVD - it needs hundreds of clips per model and is meaningless on the handful a 12 GB card can produce in a session.
Who wins what.Quality: Wan 2.2 A14B leads open weights, and the closed frontier still leads it. Speed: LTX-Video, by an order of magnitude, because of its VAE compression ratio - which is the section-3 argument made concrete. Size: Wan 2.1 1.3B is the only model that is simultaneously good, Apache-2.0 and runnable on a 12 GB card. Map that back to section 2: the ad-variant and stock-footage use cases pay for the closed frontier; the interactive/game use case needs a causal model no leaderboard row above provides; and anything running on-prem on one consumer GPU is choosing between Wan 1.3B (quality) and LTX-Video (speed).
Licences to read carefully. ModelScope T2V is CC-BY-NC-ND (non-commercial, no derivatives). CogVideoX-5B and the Tencent licences carry restrictions (the Hunyuan licence excludes the EU/UK/South Korea and forbids competitor training). Wan (2.1 and 2.2), CogVideoX-2B, Mochi 1 and Open-Sora are Apache-2.0 - if you need commercial freedom, that is the shortlist.
7. Setup
Everything below runs on a single RTX 3060 (12 GB) or on CPU (impractically slowly - a 5 s clip is tens of minutes to hours on 4 cores). Package roles:
diffusers + torch - every video pipeline here. diffusersis the general-purpose Hugging Face library for generative image/video, the same ecosystem as transformers; no vendor packages are used.
transformers - the text encoders inside those pipelines (CLIP, T5-XXL, UMT5-XXL) and the CLIP model behind our proxy metrics.
accelerate - enable_model_cpu_offload(), the single most important memory switch in this notebook.
bitsandbytes - 8-bit quantisation of the T5/UMT5 text encoder. On this box this is not optional (see below).
pyecharts, pandas - the benchmark charts and table.
The honest VRAM/RAM budget
Be clear about what does not work here. Wan 2.2 A14B (27B), HunyuanVideo (13B), Mochi 1 (10B) and CogVideoX-5B are simply out of reach on 12 GB - not “slow”, impossible without aggressive multi-bit quantisation and hours of offload thrashing. They belong in the table above, not in a cell.
Worse, the binding constraint on this machine is often not the GPU. Every model from generation 2 onwards carries a T5-XXL / UMT5-XXL text encoder: ~4.7B params, ~9.5 GB in bf16. With enable_model_cpu_offload() those weights live in system RAM - and this box has 12 GB of it, of which ~2 GB is already gone. The transformer is 1.3B; the text encoder is 4.7B. The text encoder is the model. So for LTX-Video, Wan and CogVideoX we load the text encoder in 8-bit (~5 GB), which is the difference between “runs” and “OOMs the container”.
AnimateDiff and ModelScope T2V predate that choice and use CLIP’s 123M text tower - which is exactly why they are the two fast cells in this notebook.
Expected wall clock on the 3060, at the small settings used below: ModelScope ~30-60 s, AnimateDiff ~60-90 s, LTX-Video ~1-2 min, Wan 1.3B ~4-8 min, CogVideoX-2B ~8-15 min. Video generation is slow. That is the honest headline.
# diffusers is the general-purpose HF library for generative image/video - same ecosystem as transformers.# %pip install -q torch diffusers transformers accelerate bitsandbytes imageio imageio-ffmpeg pyecharts pandas# ftfy is needed by the Wan pipeline's prompt cleaning# %pip install -q ftfy
import ctypesimport ctypes.utilimport gcimport timefrom pathlib import Pathimport torchfrom dotenv import find_dotenv, load_dotenv# Knowledge/.env sets HF_TOKEN - authenticated HF Hub requests get higher rate limitsload_dotenv(find_dotenv(usecwd=True))device ="cuda:0"if torch.cuda.is_available() else"cpu"dtype = torch.float16 if device !="cpu"else torch.float32if device !="cpu":print(torch.cuda.get_device_name(0))print("device:", device)def vram(tag=""):"Report current GPU memory (allocated / reserved). No-op on CPU."if torch.cuda.is_available(): alloc = torch.cuda.memory_allocated() /1e9 reserved = torch.cuda.memory_reserved() /1e9print(f"VRAM {tag:20s}{alloc:5.2f} GB allocated / {reserved:5.2f} GB reserved")def free_memory():"Collect garbage, empty the CUDA cache, and return freed CPU RAM to the OS." gc.collect()if torch.cuda.is_available(): torch.cuda.empty_cache() torch.cuda.ipc_collect()# glibc keeps freed CPU allocations in its arenas instead of returning them# to the OS, so RSS compounds across model sections (cpu-offloaded weights# live in system RAM). malloc_trim(0) hands the freed arenas back. See# dl-visualization-and-memory.instructions.md - not optional on a 12 GB box.try: ctypes.CDLL(ctypes.util.find_library("c") or"libc.so.6").malloc_trim(0)exceptException:passdef offload(pipe):"Pick the offload strategy for the VRAM actually free right now, not the card size."if device =="cpu":return pipeimport torch.nn as nn# bitsandbytes-quantized weights are pinned to the GPU; enable_sequential_cpu_offload# first moves the whole pipeline to CPU and STALLS on them (a hang, not a catchable# error), so a quantized pipeline must use model-level offload - the recommended path. quantized =any(getattr(m, "is_quantized", False)for m in pipe.components.values() ifisinstance(m, nn.Module)) free_gb = torch.cuda.mem_get_info()[0] /1e9# global free VRAM - counts other processesif free_gb <8.0andnot quantized:# Another process is using the card (or it is small): layer-at-a-time keeps the# peak at ~1-2 GB for a real speed cost. Free the other GPU user if you can -# check nvidia-smi on the HOST; a container only sees its own processes.# Quantized (bitsandbytes) components cannot be dispatched per-layer, so fall# back to model-level offload if sequential raises.try: pipe.enable_sequential_cpu_offload(device=device)print(f"offload: sequential ({free_gb:.1f} GB VRAM free - GPU busy, expect slow steps)")return pipeexceptExceptionas e:print(f"offload: sequential unsupported here ({type(e).__name__}) - using model-level")# Whole submodule on the GPU at a time - fast, peak ~= largest submodule (~5 GB). pipe.enable_model_cpu_offload(device=device)print(f"offload: {'quantized -> 'if quantized else''}model-level ({free_gb:.1f} GB VRAM free)")return pipe# All downloads go to DL_tasks/datasets/ (gitignored)DATA_DIR = Path("../../datasets")DATA_DIR.mkdir(exist_ok=True)HF_CACHE =str(DATA_DIR /"hf_cache")# The DiT models want bfloat16; fp16 is the fallback on pre-Ampere cards.VDTYPE = torch.bfloat16 if (device !="cpu"and torch.cuda.is_bf16_supported()) else dtypeprint("video dtype:", VDTYPE)
NVIDIA GeForce RTX 3060
device: cuda:0
video dtype: torch.bfloat16
import urllib.requestfrom diffusers.utils import export_to_giffrom IPython.display import Image as IPyImagefrom IPython.display import displayfrom PIL import ImageCLIPS_DIR = DATA_DIR /"t2v_clips"CLIPS_DIR.mkdir(exist_ok=True)# --- eval prompts: the VBench suite (the same prompt distribution the leaderboards use) ---VBENCH_PROMPTS = DATA_DIR /"vbench_all_dimension.txt"ifnot VBENCH_PROMPTS.exists():try: urllib.request.urlretrieve("https://raw.githubusercontent.com/Vchitect/VBench/master/prompts/all_dimension.txt", VBENCH_PROMPTS, )exceptExceptionas e: # offline -> fall back to the built-in prompts belowprint("could not fetch VBench prompts:", e)# One prompt is generated by EVERY model below, with the SAME seed - that is the benchmark.PROMPT ="a red panda walking through a snowy bamboo forest, camera slowly pans right"NEGATIVE ="blurry, low quality, static, watermark, jpeg artifacts, deformed"SEED =0# Download budget. Models whose download exceeds MAX_DOWNLOAD_GB stay OFF by default:# a fp32 T5/UMT5 text encoder is 10-23 GB, and 4-bit quantization happens only AFTER# the full fp32 download. Flip RUN_HEAVY to fetch + run them (needs disk + time).MAX_DOWNLOAD_GB =8RUN_HEAVY =Falseif VBENCH_PROMPTS.exists(): pool = [ln.strip() for ln in VBENCH_PROMPTS.read_text().splitlines() if ln.strip()]print(f"{len(pool)} VBench prompts available, e.g.: {pool[0]!r}")results = {} # name -> dict(frames, seconds, peak_vram, fps, size)def _to_pil_frames(frames):"Coerce a pipeline's frames (PIL, or HxWx3 numpy in [0,1] or uint8) to list[PIL.Image]."import numpy as np out = []for f in frames:ifisinstance(f, Image.Image): out.append(f)continue a = np.asarray(f)if a.dtype != np.uint8: # float in [0,1] -> 8-bit a = (a.clip(0, 1) *255).round().astype("uint8") out.append(Image.fromarray(a))return outdef run_t2v(name, call, fps, keep=8):"Time a text-to-video pipeline call, save + show the clip, and record it for section 13."if torch.cuda.is_available(): torch.cuda.reset_peak_memory_stats() t0 = time.perf_counter()# Some pipelines (ModelScope T2V) return numpy frames, not PIL; normalise here. frames = _to_pil_frames(call()) secs = time.perf_counter() - t0 peak = torch.cuda.max_memory_allocated() /1e9if torch.cuda.is_available() else0.0 path = CLIPS_DIR /f"{name}.gif" export_to_gif(frames, str(path), fps=fps) step =max(1, len(frames) // keep) results[name] = {"frames": [f.convert("RGB") for f in frames[::step][:keep]], # subsample; CPU-side, tiny"seconds": secs,"peak_vram": peak,"fps": fps,"n_frames": len(frames),"size": f"{frames[0].width}x{frames[0].height}","duration": len(frames) / fps, }print(f"{name}: {secs:.1f}s {len(frames)} frames @ {fps} fps "f"({len(frames) / fps:.1f}s of video) {results[name]['size']} peak {peak:.2f} GB") display(IPyImage(filename=str(path)))return framesdef contact_sheet(frames, cols=4, width=192):"Tile frames into a single PIL image - the fastest way to eyeball temporal coherence." scale = width / frames[0].width h =int(frames[0].height * scale) rows = (len(frames) + cols -1) // cols sheet = Image.new("RGB", (cols * width, rows * h), "white")for i, f inenumerate(frames): sheet.paste(f.convert("RGB").resize((width, h)), ((i % cols) * width, (i // cols) * h))return sheetdef text_encoder_4bit(cls, repo, subfolder="text_encoder"):"Load a T5/UMT5 text encoder in 4-bit NF4 - roughly half the load-time RAM spike and the resident VRAM of 8-bit; the encoder, not the DiT, is the memory hog on this box." kw =dict(subfolder=subfolder, torch_dtype=VDTYPE, cache_dir=HF_CACHE, low_cpu_mem_usage=True)if device =="cpu":return cls.from_pretrained(repo, **kw) # bitsandbytes needs CUDAfrom transformers import BitsAndBytesConfig quant = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=VDTYPE )return cls.from_pretrained(repo, quantization_config=quant, **kw)
946 VBench prompts available, e.g.: 'In a still frame, a stop sign'
8. ModelScope T2V - the 2023 baseline (1.7B, fast, crude)
ali-vilab/text-to-video-ms-1.7b is the original open text-to-video model: an inflated Stable Diffusion UNet with temporal layers, a CLIP text encoder, a 2-D (per-frame) VAE, and 16 frames at 256x256. It is here as the control: it shows you exactly what “generation 1” bought you and what it did not.
Expect a recognisable subject, a watermark-ish texture inherited from its WebVid stock-footage training data, visible flicker, and almost no coherent camera motion - in under a minute. Note the licence: CC-BY-NC-ND 4.0, non-commercial and no derivatives.
Pick it when: you want a smoke test, or a historical reference. Never for production.
from diffusers import DiffusionPipeline, DPMSolverMultistepSchedulerms_pipe = DiffusionPipeline.from_pretrained("ali-vilab/text-to-video-ms-1.7b", torch_dtype=dtype, variant="fp16"if device !="cpu"elseNone, cache_dir=HF_CACHE,)ms_pipe.scheduler = DPMSolverMultistepScheduler.from_config(ms_pipe.scheduler.config)ms_pipe.enable_vae_slicing()if device !="cpu": offload(ms_pipe) # keeps only the active submodule on the GPUelse: ms_pipe.to(device)frames = run_t2v("modelscope-t2v-1.7b",lambda: ms_pipe( prompt=PROMPT, negative_prompt=NEGATIVE, num_frames=16, num_inference_steps=25, generator=torch.Generator("cpu").manual_seed(SEED), ).frames[0], fps=8,)display(contact_sheet(frames[::2]))del ms_pipe, framesfree_memory()vram("after ModelScope")
The TextToVideoSDPipeline has been deprecated and will not receive bug fixes or feature updates after Diffusers version 0.33.1.
/home/bthek1/Knowledge/.venv/lib/python3.14/site-packages/diffusers/pipelines/pipeline_utils.py:2273: FutureWarning: `enable_vae_slicing` is deprecated and will be removed in version 0.40.0. Calling `enable_vae_slicing()` on a `TextToVideoSDPipeline` is deprecated and this method will be removed in a future version. Please use `pipe.vae.enable_slicing()`.
deprecate(
VRAM after ModelScope 0.01 GB allocated / 0.02 GB reserved
9. AnimateDiff - motion adapters on Stable Diffusion 1.5
AnimateDiff is the cleverest thing in generation 1: instead of training a video model, train only the temporal layers (a ~450M “motion adapter”) and make them plug into any SD 1.5 UNet. Every community checkpoint, LoRA and style you have from the image world keeps working, and you get motion for free.
Base checkpoint: SG161222/Realistic_Vision_V5.1_noVAE (swap for any SD1.5 finetune)
Text encoder is CLIP (123M), so nothing here strains RAM: ~6 GB VRAM, ~60-90 s for 16 frames at 512x512. The trade-off is the generation-1 trade-off: strong per-frame aesthetics (it is SD 1.5, after all), weak global motion - the camera rarely goes anywhere and long-range coherence is a slideshow. wangfuyun/AnimateLCM is the distilled variant if you want it in 4-8 steps.
Pick it when: you want stylised, fast, LoRA-controllable animation on a small GPU - the single most practical option here.
/home/bthek1/Knowledge/.venv/lib/python3.14/site-packages/huggingface_hub/utils/_validators.py:205: UserWarning: The `local_dir_use_symlinks` argument is deprecated and ignored in `hf_hub_download`. Downloading to a local directory does not use symlinks anymore.
warnings.warn(
The config attributes {'motion_activation_fn': 'geglu', 'motion_attention_bias': False, 'motion_cross_attention_dim': None} were passed to MotionAdapter, but are not expected and will be ignored. Please verify your config.json configuration file.
There are modules in UNetMotionModel that should be kept in float32: []. Casting directly with `to()` can lead to inconsistent results; set `torch_dtype` in `from_pretrained()` instead to keep these modules in float32.
VRAM after AnimateDiff 0.01 GB allocated / 0.02 GB reserved
10. LTX-Video 2B - the speed play
Lightricks’ Lightricks/LTX-Video is the first DiT built around the token-count equation from section 3. Its Video-VAE compresses 8x in time and 32x in space (a ~1:192 pixel-to-latent ratio), so 121 frames at 768x512 is only ~6k latent tokens - a third of what CogVideoX needs for half as many frames. Quadratic attention then does the rest of the work for you.
The compression is paid for at the decoder: the VAE decoder performs the final denoising step itself, because at 1:192 there is genuinely not enough high-frequency information left in the latent. Fine detail is the weak point; motion and speed are the strength.
Here: 512x320, 65 frames, 30 steps - a ~2.7 s clip in roughly a minute or two on a 3060. The T5-XXL text encoder is loaded in 8-bit; without that, its ~9.5 GB of bf16 weights in system RAM would take the container down. The -distilled checkpoints (Lightricks/LTX-Video-0.9.7-distilled) run in 4-10 steps with guidance_scale=1.0. Note that the current flagship, LTX-2 (14B video + 5B audio, native synchronised sound), is far outside this budget.
Pick it when: throughput matters, or you want the closest thing to interactive iteration on a consumer card.
ifnot RUN_HEAVY: # ~LTX-Video needs a ~20 GB download (19 GB fp32 T5-XXL) > 8 GB capprint("skipped: LTX-Video needs a ~20 GB download (19 GB fp32 T5-XXL) > 8 GB cap. Set RUN_HEAVY=True in the setup cell to run it.")else:from diffusers import LTXPipelinefrom transformers import T5EncoderModel ltx_id ="Lightricks/LTX-Video" ltx_pipe = LTXPipeline.from_pretrained( ltx_id, text_encoder=text_encoder_4bit(T5EncoderModel, ltx_id), # 4.7B -> ~5 GB instead of ~9.5 GB torch_dtype=VDTYPE, cache_dir=HF_CACHE, ) ltx_pipe.vae.enable_slicing() ltx_pipe.vae.enable_tiling()if device !="cpu": offload(ltx_pipe)else: ltx_pipe.to(device) frames = run_t2v("ltx-video-2b",lambda: ltx_pipe( prompt=PROMPT +". The scene is captured in crisp detail with natural lighting.", negative_prompt="worst quality, inconsistent motion, blurry, jittery, distorted", width=512, height=320, num_frames=65, # must be 8k + 1 num_inference_steps=30, generator=torch.Generator("cpu").manual_seed(SEED), ).frames[0], fps=24, ) display(contact_sheet(frames[::8]))del ltx_pipe, frames free_memory() vram("after LTX-Video")
skipped: LTX-Video needs a ~20 GB download (19 GB fp32 T5-XXL) > 8 GB cap. Set RUN_HEAVY=True in the setup cell to run it.
11. Wan 2.1 T2V 1.3B - the one that actually belongs on a consumer card
Wan-AI/Wan2.1-T2V-1.3B-Diffusers is the standout of this notebook: Apache-2.0, 1.3B DiT, 3-D causal VAE (4x8x8), flow matching, and it produces genuinely coherent 480p motion at a size no other serious model matches. Alibaba’s own claim is ~8.19 GB of VRAM, which is exactly the point - it was designed for this.
Two things to know:
The 1.3B is the DiT. The text encoder is UMT5-XXL, 4.7B. That asymmetry is the whole story of this box. In 8-bit it is ~5 GB of RAM; in bf16 it is ~9.5 GB and the container dies. The multilingual UMT5 encoder is also why Wan understands long, compositional, Chinese-or-English prompts far better than the CLIP-conditioned models above - the strong text encoder is not incidental, it is most of the prompt-adherence win.
flow_shift shifts the flow-matching timestep schedule: 3.0 for 480p, 5.0+ for 720p. Getting it wrong produces mush.
Settings here: 480x832 is the native aspect, but we run 480x272 with 33 frames to keep this to ~4-8 minutes on a 3060. At the native 81 frames / 480x832 expect 15-30 minutes per clip on this card. The 14B sibling and Wan 2.2’s MoE A14B are strictly better and strictly out of reach.
Pick it when: you want the best open quality that fits, and Apache-2.0 licensing.
ifnot RUN_HEAVY: # ~Wan 2.1 needs a ~29 GB download (22.7 GB fp32 UMT5-XXL) > 8 GB capprint("skipped: Wan 2.1 needs a ~29 GB download (22.7 GB fp32 UMT5-XXL) > 8 GB cap. Set RUN_HEAVY=True in the setup cell to run it.")else:from diffusers import AutoencoderKLWan, WanPipelinefrom diffusers.schedulers.scheduling_unipc_multistep import UniPCMultistepSchedulerfrom transformers import UMT5EncoderModel wan_id ="Wan-AI/Wan2.1-T2V-1.3B-Diffusers"# VAE stays in fp32 - the Wan docs are explicit that fp16 decoding degrades quality badly. wan_vae = AutoencoderKLWan.from_pretrained( wan_id, subfolder="vae", torch_dtype=torch.float32, cache_dir=HF_CACHE ) wan_pipe = WanPipeline.from_pretrained( wan_id, vae=wan_vae, text_encoder=text_encoder_4bit(UMT5EncoderModel, wan_id), # the 4.7B elephant torch_dtype=VDTYPE, cache_dir=HF_CACHE, ) wan_pipe.scheduler = UniPCMultistepScheduler.from_config( wan_pipe.scheduler.config, flow_shift=3.0# 3.0 for <=480p, 5.0+ for 720p )if device !="cpu": offload(wan_pipe)else: wan_pipe.to(device) frames = run_t2v("wan2.1-t2v-1.3b",lambda: wan_pipe( prompt=PROMPT, negative_prompt=NEGATIVE, width=480, height=272, num_frames=33, # must be 4k + 1; native is 81 num_inference_steps=25, # native is 50 - halved to keep this under ~8 min guidance_scale=5.0, generator=torch.Generator("cpu").manual_seed(SEED), ).frames[0], fps=16, ) display(contact_sheet(frames[::4]))del wan_pipe, wan_vae, frames free_memory() vram("after Wan 2.1")
skipped: Wan 2.1 needs a ~29 GB download (22.7 GB fp32 UMT5-XXL) > 8 GB cap. Set RUN_HEAVY=True in the setup cell to run it.
12. CogVideoX-2B - marginal, and honest about it
THUDM/CogVideoX-2b (Apache-2.0) is the reference implementation of the generation-2 idea: a 3-D causal VAE (4x8x8) plus an “expert” DiT with adaptive LayerNorm for the text/video modality split, and full 3-D attention. It is worth running once because it is the model the compression argument in section 3 was built on.
But be clear: it is marginal on a 3060. The diffusers memory table for this family lists ~19 GB with enable_model_cpu_offload() alone, dropping to ~11 GB once vae.enable_tiling() is on - and that is before you account for the 4.7B T5-XXL encoder sitting in 12 GB of system RAM. We therefore stack every switch we have: 8-bit text encoder, model CPU offload, VAE slicing and tiling, 49 frames at 480x720 reduced to 240x360, and 20 steps instead of 50.
Expect 8-15 minutes, and expect quality below Wan 1.3B at four times the wall clock. That is not a bug in the notebook; it is the two-year gap between CogVideoX (2024) and Wan (2025) showing up as compute. CogVideoX-5B is better and does not fit at all here.
Pick it when: you specifically want the CogVideoX family (its LoRA/fine-tuning ecosystem via finetrainers is mature). Otherwise, use Wan.
ifnot RUN_HEAVY: # ~CogVideoX-5b needs a ~21 GB download (fp32 T5-XXL) > 8 GB capprint("skipped: CogVideoX-5b needs a ~21 GB download (fp32 T5-XXL) > 8 GB cap. Set RUN_HEAVY=True in the setup cell to run it.")else:from diffusers import CogVideoXPipeline cog_id ="THUDM/CogVideoX-2b" cog_pipe = CogVideoXPipeline.from_pretrained( cog_id, text_encoder=text_encoder_4bit(T5EncoderModel, cog_id), torch_dtype=dtype, # the 2b checkpoint is trained in fp16 (the 5b one wants bf16) cache_dir=HF_CACHE, )# Every memory switch we have. Without tiling this OOMs a 12 GB card. cog_pipe.vae.enable_slicing() cog_pipe.vae.enable_tiling()if device !="cpu": offload(cog_pipe)else: cog_pipe.to(device) frames = run_t2v("cogvideox-2b",lambda: cog_pipe( prompt=PROMPT +". The lighting is soft and the atmosphere is peaceful and cinematic.", negative_prompt=NEGATIVE, width=320, # must be divisible by 16 height=240, num_frames=49, # 4k + 1, matching the VAE's 4x temporal compression num_inference_steps=20, # native is 50 guidance_scale=6.0, generator=torch.Generator("cpu").manual_seed(SEED), ).frames[0], fps=8, ) display(contact_sheet(frames[::6]))del cog_pipe, frames free_memory() vram("after CogVideoX")
skipped: CogVideoX-5b needs a ~21 GB download (fp32 T5-XXL) > 8 GB cap. Set RUN_HEAVY=True in the setup cell to run it.
13. Head-to-head Benchmark
Every model above generated the same prompt with the same seed, and run_t2v recorded the clip, the wall clock and the peak VRAM as it went. Nothing is regenerated here - we reload the 150M CLIP model (now that every video pipeline has been freed, it is the only thing live) and score what is already in results with the three proxies from section 4.
The metrics:
text alignment - mean CLIP(prompt, frame). Higher is better. Blind to motion.
temporal consistency - mean CLIP(frame_t, frame_t+1). Higher is better only if dynamic degree is not near zero, or you are rewarding a still image.
dynamic degree - mean absolute pixel delta between adjacent frames. Sanity check on the above.
s / s-of-video - wall clock divided by clip duration. This, not seconds-per-clip, is the comparable speed number: the models below produce 2 s to 6 s of video each.
Read this as a smoke test, not a leaderboard. One prompt, one seed, one clip per model, at deliberately reduced resolutions and step counts on an RTX 3060 (12 GB) / 4 vCPU / 12 GB RAM box. Real evaluation means the full VBench suite (~950 prompts, 5 seeds) on hardware that does not need enable_model_cpu_offload(), and even then the ranking that matters is human Elo. Each model here also ran at a different resolution and frame count because that is what fit - so the speed column compares configurations, not architectures on equal footing.
import pandas as pd# Reload CLIP (freed back in section 4). The scoring functions read these globals.clip_model = CLIPModel.from_pretrained(clip_id, cache_dir=HF_CACHE).to(dev).eval()clip_proc = CLIPProcessor.from_pretrained(clip_id, cache_dir=HF_CACHE)rows = []for name, r in results.items(): align, _ = text_alignment(r["frames"], PROMPT) cons, _ = temporal_consistency(r["frames"]) rows.append({"model": name,"text_align": round(align, 4),"temporal_cons": round(cons, 4),"dynamic": round(dynamic_degree(r["frames"]), 4),"seconds": round(r["seconds"], 1),"s_per_s_video": round(r["seconds"] / r["duration"], 1),"peak_vram_gb": round(r["peak_vram"], 2),"resolution": r["size"],"frames": r["n_frames"], })df = pd.DataFrame(rows).sort_values("text_align", ascending=False).reset_index(drop=True)# CLIP was the last model live - release it now that scoring is done.del clip_model, clip_procfree_memory()vram("final")df
VRAM final 0.01 GB allocated / 0.02 GB reserved
model
text_align
temporal_cons
dynamic
seconds
s_per_s_video
peak_vram_gb
resolution
frames
0
animatediff-sd15
0.3741
0.9878
0.0281
57.3
28.7
4.85
512x512
16
1
modelscope-t2v-1.7b
0.3487
0.9495
0.1084
19.2
9.6
3.55
256x256
16
from pyecharts import options as optsfrom pyecharts.charts import Barmodels = df["model"].tolist()bar = ( Bar() .add_xaxis(models) .add_yaxis("text alignment (CLIPSIM)", df["text_align"].tolist()) .add_yaxis("temporal consistency", df["temporal_cons"].tolist()) .add_yaxis("dynamic degree", df["dynamic"].tolist()) .set_series_opts(label_opts=opts.LabelOpts(is_show=False)) .set_global_opts( title_opts=opts.TitleOpts( title="Text-to-video proxies, same prompt + same seed", subtitle="RTX 3060 12 GB - 1 prompt, 1 clip per model: a smoke test, not a leaderboard", ), xaxis_opts=opts.AxisOpts(axislabel_opts=opts.LabelOpts(rotate=20)), yaxis_opts=opts.AxisOpts(name="score"), tooltip_opts=opts.TooltipOpts(trigger="axis"), legend_opts=opts.LegendOpts(pos_top="12%"), datazoom_opts=[opts.DataZoomOpts(type_="inside")], ))bar.render_notebook()
from pyecharts.charts import Scatter# Quality vs cost: text alignment against seconds of compute per second of video produced.scatter = Scatter().add_xaxis([float(v) for v in df["s_per_s_video"]])for _, r in df.iterrows(): scatter.add_yaxis( r["model"], [[float(r["s_per_s_video"]), float(r["text_align"])]], symbol_size=18, label_opts=opts.LabelOpts(is_show=False), )scatter.set_global_opts( title_opts=opts.TitleOpts( title="Text alignment vs cost", subtitle="x = seconds of compute per second of generated video (lower is better)", ), xaxis_opts=opts.AxisOpts(type_="value", name="s of compute / s of video"), yaxis_opts=opts.AxisOpts(type_="value", name="CLIP text alignment", min_="dataMin"), tooltip_opts=opts.TooltipOpts(trigger="item"), legend_opts=opts.LegendOpts(pos_top="12%"),)scatter.render_notebook()
14. Common Frameworks
Text-to-video shares its ecosystem with image-to-video almost entirely - same library, same quantisation tricks, same trainers - so the interesting difference is what sits in front of the model. These models are trained on dense, structured captions written by a VLM, which makes a terse human prompt off-distribution. Every serious system therefore has an LLM rewriting the prompt before the diffusion model ever sees it, and that rewriter is as much a part of the stack as the DiT.
Subject consistency, motion smoothness, aesthetic quality and text-video alignment, as a runnable harness
Apache 2.0
Before believing any comparison. Four cherry-picked clips are not evidence
The 2026 default stack is an LLM prompt rewriter, diffusers with a distilled Wan or LTX checkpoint quantised to NF4/fp8, ComfyUI for the graph, and VBench to decide. If you need real control, generate the first frame with a text-to-image model and switch to 07_Image_to_Video.
The common wrong turn is spending on the model when the prompt is the problem: a rewritten, densely structured prompt routinely beats a larger checkpoint on the same hardware. The second is forgetting that open video is silent. Audio is generated separately and aligned by hand (Audio/01_Text_to_Audio) unless you can run one of the joint audio-video models, which do not fit here.
15. Going Further
Fine-tuning and LoRA. Video LoRA is real and widely used - motion styles, characters, brand looks. finetrainers (CogVideoX, LTX, Wan, Hunyuan) and diffusion-pipe are the standard trainers. Be honest about the cost: a Wan 1.3B LoRA needs ~12-16 GB with gradient checkpointing + 8-bit optimizer (i.e. right at or past this box’s limit), a 14B LoRA wants 24-48 GB, and full fine-tuning is a multi-node exercise. LTX-2 ships an official LoRA trainer. Datasets are small by LLM standards - 20-100 clips is enough for a style LoRA.
Prompt engineering, and the trick everyone uses: prompt rewriting. Video models are trained on dense, structured captions produced by a VLM, so a terse human prompt is off-distribution. Every serious system rewrites it first with an LLM into subject + action + camera + lighting + style (Wan and CogVideoX both ship a prompt-extension script; the closed APIs do it silently). This is the single highest-leverage thing you can do before touching a hyperparameter.
Control beyond text. For any real degree of control, text alone is the wrong interface: - Condition on a first frame (07_Image_to_Video) - generate the frame with a T2I model you can steer precisely, then animate. Almost every production “text-to-video” pipeline is really this. - ControlNet-for-video / pose-driven: Wan VACE (unified reference/pose/depth/mask conditioning, and 1.3B so it fits), CogVideoX-Fun-Pose, Wan 2.2 Animate. See 18_Video_to_Video. - Camera control: MotionCtrl, CameraCtrl, and native camera-motion LoRAs.
The audio gap. Open video is silent. Veo 3 generates dialogue, SFX and ambience in the same pass, and it is the most-felt capability difference in practice; LTX-2 (Jan 2026) is the first open model to answer it (14B video + 5B audio, joint diffusion). Until you can run that, the pragmatic route is generating audio separately and aligning it - see Audio/01_Text_to_Audio.
Streaming / interactive. If you need frames as they generate, look at Self-Forcing and CausVid - causal, KV-cached, distilled from a bidirectional teacher. This is where the field is going, and it is the only path to interactive world models.