* feat(vllm-cpp): serve MiniMax-H3 video+audio generation
vllm.cpp's C ABI grew a video slice (ABI v12): a second engine handle
loaded from the MiniMax-H3 checkpoint SET, one blocking generate, and a
composed ffmpeg argv the caller execs. This wires that into LocalAI's
existing /video endpoint, so `vllm-cpp` now serves both text and video
and a clip comes back as an MP4 with a real audio track rather than a
silent render.
The video engine is a separate handle rather than a mode of the text
one because H3 is not a model directory: the DiT, the text encoder and
two VAEs are separate artifacts, and vllm.cpp has the two loaders refuse
each other's checkpoints. `Load` takes the video branch when the config
declares any of the video options; `parameters.model` is the DiT and the
rest of the set is named in `options:`.
Three details are worth calling out because getting them wrong is
expensive:
- The partition is DECLARED, not detected. The community quantisations
strip the release metadata and the FL2VA and Ref2VA DiTs are
byte-structurally identical, so the engine refuses to generate until
it is told which it has. Worse, a mismatch does not fail cleanly: a
reference passed to an FL2VA DiT renders for hours and returns a
coloured lattice over the frame. The backend refuses that combination
up front instead.
- ffmpeg comes from the host. libvllm writes frames plus a WAV and
composes the mux argv, then spawns nothing - that process boundary is
upstream's decision. The backend execs it, the same arrangement
vibevoice-cpp uses for transcoding, and ffmpeg also converts a
start_image upload into the binary PPM at the exact output canvas the
engine requires.
- It is slow. Roughly 176 s per denoise step at the default 1344x768
canvas on a 20-SM device, so the 50-step default is a multi-hour job.
Nothing on this path imposes a deadline.
The /video endpoint no longer forces 512x512 when the request omits the
geometry. Every video backend already supplies its own default for a
zero (512x512 for stablediffusion-ggml, 1280x720 for diffusers, 832x480
for longcat-video, 1344x768 for H3), so the hardcoded value only ever
overrode the model's trained canvas with one three of the four were
never trained at.
Moving the engine pin from ABI v10 to v16 also grows the text
vllm_model_params mirror by the v14 device field and the v16 KV-sizing
knobs. LocalAI sets none of them - 0 is the pre-v14 engine byte for byte
- but the struct SIZE is part of the layout contract, so leaving them
out would have vllm_engine_load read past the allocation.
Gallery: `minimax-h3-fl2va-q4` installs the Q4_K_M FL2VA set (~40 GB
across five weight files plus the two VAE configs that carry the latent
statistics).
Assisted-by: Claude:claude-opus-5 golangci-lint yamllint go-vet
* fix(vllm-cpp): unbreak the Darwin build at the new engine pin
src/capi/vllm_c.cpp opens one `extern "C" {` for the whole ABI surface,
so file-local helpers declared inside it inherit C linkage. The video
slice added one that returns std::string, which Apple Clang reports as
-Wreturn-type-c-linkage and vllm.cpp's target-local -Werror turns into a
build failure. GCC and upstream Clang do not diagnose it, so only the
metal-darwin-arm64 job saw it.
Suppress it the same way this Makefile already suppresses Apple Clang's
-Wgnu-folding-constant on the Metal build. The helper is never called
across the boundary so the warning describes no hazard here, but it is a
real upstream wart: the fix belongs in vllm.cpp, hoisting the helper
above the extern "C" block, and this flag should go when a pin carrying
that fix lands.
Assisted-by: Claude:claude-opus-5
* fix(vllm-cpp): patch the engine clone instead of the warning flag
The -Wno-return-type-c-linkage added in the previous commit does nothing.
vllm_cpp_set_warnings adds `-Wall -Wextra -Werror` as PRIVATE target
options, so they land after anything CMAKE_CXX_FLAGS contributes, and
-Wall re-enables the -Wreturn-type group that -Wreturn-type-c-linkage
belongs to. The darwin job failed again on the same line, which is the
evidence: a consumer cannot wave this off from outside the engine.
Position is the only fix, so carry it as a patch against the pinned SHA,
the way longcat-video patches its own upstream. It hoists the helper
above the `extern "C" {` that gives it C linkage; it is file-local and
never called across the boundary, so nothing else moves.
`git apply` is unguarded on purpose: a patch that stops applying must
fail the clone loudly, because the alternative is a pin that silently
ships without a fix it is documented to carry. The patch header names
what retires it - a pin carrying the fix upstream, where it belongs.
Verified by applying the patch with `git apply` to the exact blob at the
pinned SHA and diffing the result against the intended file.
Assisted-by: Claude:claude-opus-5
* chore(vllm-cpp): bump the engine pin to ABI v17 and drop the vendored OrEmpty patch
The OrEmpty linkage fix this backend carried as patches/0001-* landed upstream
(mudler/vllm.cpp#195, 7534da65), so the patch has done its job. It is deleted
rather than left in place: the Makefile applies patches/*.patch unguarded and
documents that "a patch that no longer applies must FAIL the clone", so keeping
it against fixed source would break the build the moment the pin moved. Bumping
the pin and deleting the patch therefore have to be the SAME change.
Pin f921062b -> 776c56f1 (current vllm.cpp main).
That range also carries the engine's ABI v17 (vllm_server_main: the OpenAI server
published on the public surface). registerLib compares the library's
vllm_abi_version against `abiVersion` for EXACT equality, so the constant moves
16 -> 17 in the same commit or every load fails with an ABI mismatch.
The bump is safe for the layout assertions in video_test.go: diffing include/vllm.h
across the two pins shows zero struct-field changes -- v17 adds one function
declaration, the version macro and a doc comment, nothing else -- so every
unsafe.Offsetof in the video params test still holds.
Assisted-by: Claude Code:claude-opus-5 [ClaudeCode]
* chore(vllm-cpp): re-pin to pick up the VLLM_CPP_SERVER=OFF link fix
The previous pin carried vllm.cpp's ABI v17 (vllm_server_main) but not the guard
that makes it link when the server is compiled out. This backend builds libvllm
with VLLM_CPP_SERVER off, so the darwin lane failed at the dylib link with
vllm::entrypoints::openai::VllmServerMain undefined.
Fixed upstream in mudler/vllm.cpp#202: the C entry point is now guarded, so the
symbol is still exported (ABI v17 stays resolvable for dlopen) while the
no-server arm reports the missing capability instead of dragging in a translation
unit that was never compiled.
Verified upstream in BOTH arms before re-pinning: SERVER=ON builds and runs, and
SERVER=OFF configures, links, produces libvllm.so, and `nm -D` shows
vllm_server_main exported next to vllm_video_generate and vllm_transcribe.
Assisted-by: Claude Code:claude-opus-5 [ClaudeCode]
---------
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
20 KiB
+++ disableToc = false title = "Video Generation" weight = 51 url = "/features/video-generation/" aliases = ["/features/longcat-video/"] +++
LocalAI can generate videos from text prompts and optional image or audio conditioning via the /video endpoint. Supported backends include diffusers, stablediffusion, vllm-omni, vllm-cpp (MiniMax-H3, which generates video and audio together), and the dedicated longcat-video backend.
API
- Method:
POST - Endpoint:
/video
Request
The request body is JSON with the following fields:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
model |
string |
Yes | Model name to use | |
prompt |
string |
Yes | Text description of the video to generate | |
negative_prompt |
string |
No | What to exclude from the generated video | |
start_image |
string |
No | Starting image as base64 string or URL | |
end_image |
string |
No | Ending image for guided generation | |
audio |
string |
No | Audio conditioning as base64, a data URI, or URL | |
width |
int |
No | backend | Video width in pixels; omit it to get the model's own default canvas |
height |
int |
No | backend | Video height in pixels; omit it to get the model's own default canvas |
num_frames |
int |
No | Number of frames | |
fps |
int |
No | Frames per second | |
seconds |
string |
No | Duration in seconds | |
size |
string |
No | Size specification (alternative to width/height) | |
input_reference |
string |
No | Input reference for the generation | |
seed |
int |
No | Random seed for reproducibility | |
cfg_scale |
float |
No | Classifier-free guidance scale | |
step |
int |
No | Number of inference steps | |
response_format |
string |
No | url |
url to return a file URL, b64_json for base64 output |
params |
object |
No | Backend-specific string parameters |
Response
Returns an OpenAI-compatible JSON response:
| Field | Type | Description |
|---|---|---|
created |
int |
Unix timestamp of generation |
id |
string |
Unique identifier (UUID) |
data |
array |
Array of generated video items |
data[].url |
string |
URL path to video file (if response_format is url) |
data[].b64_json |
string |
Base64-encoded video (if response_format is b64_json) |
Usage
First install a video-generation model from the gallery (the examples below use longcat-video):
local-ai run longcat-video
Generate a video from a text prompt
curl http://localhost:8080/video \
-H "Content-Type: application/json" \
-d '{
"model": "longcat-video",
"prompt": "A cat playing in a garden on a sunny day",
"width": 512,
"height": 512,
"num_frames": 16,
"fps": 8
}'
Example response
{
"created": 1709900000,
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"data": [
{
"url": "/generated-videos/abc123.mp4"
}
]
}
Generate with a starting image
curl http://localhost:8080/video \
-H "Content-Type: application/json" \
-d '{
"model": "longcat-video",
"prompt": "A timelapse of flowers blooming",
"start_image": "https://example.com/flowers.jpg",
"num_frames": 24,
"fps": 12,
"seed": 42,
"cfg_scale": 7.5,
"step": 30
}'
Get base64-encoded output
curl http://localhost:8080/video \
-H "Content-Type: application/json" \
-d '{
"model": "longcat-video",
"prompt": "Ocean waves on a beach",
"response_format": "b64_json"
}'
LongCat-Video and Avatar 1.5
LocalAI's longcat-video backend serves Meituan's official LongCat video-generation models through the /video API and the Studio Video page.
| Gallery model | Upstream checkpoint | Inputs | Output |
|---|---|---|---|
longcat-video |
meituan-longcat/LongCat-Video |
text, optional start image | video |
longcat-video-avatar-1.5 |
meituan-longcat/LongCat-Video-Avatar-1.5 |
text, audio, optional portrait | video with the source audio |
The base checkpoint supports text-to-video and image-to-video. Avatar 1.5 adds audio-driven character animation, optional portrait conditioning, and continuation segments for longer speech.
{{% notice warning %}} LongCat is a large, CUDA-only model family. LocalAI publishes this backend for Linux with NVIDIA CUDA 12 or CUDA 13 on x86_64 and CUDA 13 on ARM64. CPU, ROCm, and macOS images are not available. Avatar 1.5 also loads components from the base checkpoint, so reserve substantial disk and GPU or unified memory. {{% /notice %}}
Install from the Model Gallery
Install one or both recipes from Models in the web UI, or use the CLI:
local-ai models install longcat-video
local-ai models install longcat-video-avatar-1.5
You can also import either official Hugging Face URL. The importer recognizes the two repositories and writes a longcat-video model config with the appropriate use case and input/output modalities.
The required OCI backend is installed automatically when LocalAI first loads the model. The hardware detector selects the CUDA 12, CUDA 13, or CUDA 13 ARM64 variant.
DGX Spark and NVIDIA ARM64
Use a LocalAI CUDA 13 ARM64 image as described in [GPU acceleration]({{%relref "features/GPU-acceleration" %}}). The backend defaults to PyTorch SDPA, avoiding the FlashAttention dependency that is commonly unavailable on Blackwell ARM64 systems.
For unified-memory systems, start with BF16 (use_int8:false, the default). INT8 lowers steady-state DiT memory but can have a higher load-time peak because the full model is materialized before the quantized weights are applied.
Generate in Studio
- Open Studio, then choose Video.
- Select
longcat-videoorlongcat-video-avatar-1.5. - Enter a prompt and choose
832x480or1280x720. - Expand Reference media to upload a start image. For Avatar 1.5, upload or record the speech under Avatar audio.
- Select Generate.
The base model can run without a reference image for text-to-video. Avatar 1.5 requires audio; the portrait is optional.
LongCat API examples
Text-to-video
curl http://localhost:8080/video \
-H "Content-Type: application/json" \
-d '{
"model": "longcat-video",
"prompt": "A cinematic tracking shot through a misty redwood forest",
"width": 832,
"height": 480,
"num_frames": 93,
"fps": 15
}'
Image-to-video
start_image accepts raw base64, a browser-style data URI, or a public HTTP(S) URL:
curl http://localhost:8080/video \
-H "Content-Type: application/json" \
-d "{
\"model\": \"longcat-video\",
\"prompt\": \"The subject turns toward the camera as leaves move in the breeze\",
\"start_image\": \"$(base64 --wrap=0 portrait.png)\",
\"params\": {
\"resolution\": \"480p\"
}
}"
Avatar from speech and a portrait
audio accepts raw base64, a data URI, or a public HTTP(S) URL. Each staged image or audio input is limited to 128 MiB.
curl http://localhost:8080/video \
-H "Content-Type: application/json" \
-d "{
\"model\": \"longcat-video-avatar-1.5\",
\"prompt\": \"A friendly presenter speaking naturally to camera\",
\"start_image\": \"$(base64 --wrap=0 portrait.png)\",
\"audio\": \"$(base64 --wrap=0 speech.wav)\",
\"width\": 832,
\"height\": 480,
\"params\": {
\"offload_kv_cache\": \"true\"
}
}"
Avatar output is generated at 25 FPS and is muxed with the submitted audio. When neither num_frames nor params.num_segments is provided, LocalAI derives the continuation count from the audio duration, up to the model's max_segments setting.
LongCat model configuration
The gallery and importer make each model self-describing. A manual Avatar 1.5 config looks like this:
name: longcat-video-avatar-1.5
backend: longcat-video
known_usecases:
- video
known_input_modalities:
- text
- image
- audio
known_output_modalities:
- video
options:
- attention_backend:sdpa
- use_distill:true
- max_segments:8
parameters:
model: meituan-longcat/LongCat-Video-Avatar-1.5
The explicit modality declarations are used by GET /v1/models/capabilities and attachment-aware clients. They avoid inferring model behavior from backend or checkpoint names.
Load options
Model load options use key:value entries in options:
| Option | Default | Description |
|---|---|---|
attention_backend |
sdpa |
sdpa, auto, flash2, flash3, or xformers; packaged images guarantee sdpa |
use_distill |
Avatar: true; base: false |
Use the checkpoint's accelerated distillation path |
use_int8 |
false |
Use Avatar 1.5's INT8 DiT; unsupported by the base model |
base_model |
meituan-longcat/LongCat-Video |
Base tokenizer, text encoder, and VAE used by Avatar 1.5 |
max_segments |
8 |
Maximum continuation segments accepted for one request |
resolution |
480p |
Default image-conditioned resolution: 480p or 720p |
The initial backend supports one GPU per process. Tensor or context parallel sizes above one are rejected.
Per-request parameters
The /video request's params object accepts string values:
| Parameter | Description |
|---|---|
num_segments |
Explicit number of Avatar continuation segments |
audio_guidance_scale |
Audio classifier-free guidance when distillation is disabled |
offload_kv_cache |
Offload continuation KV cache (true or false) |
ref_img_index |
Reference-frame index used during continuation |
mask_frame_range |
Number of frames blended around continuation boundaries |
resolution |
Per-request image-conditioned resolution (480p or 720p) |
With distillation enabled, Avatar uses eight inference steps and fixed text/audio guidance of 1.0. Disable use_distill in the model config before tuning step, cfg_scale, or audio_guidance_scale.
LongCat troubleshooting
- HTTP 400, audio is required: Avatar 1.5 was selected without
audio. - HTTP 400, request needs too many segments: trim the audio or raise
max_segmentsin the model options. - HTTP 412: the installed LocalAI runtime cannot select a compatible NVIDIA backend image.
- Out of memory while loading: use BF16 on unified-memory hardware, close other GPU workloads, or reduce model concurrency. INT8 is not guaranteed to reduce peak load memory.
- Slow first request: the backend and checkpoints are downloaded and loaded on demand; subsequent requests reuse the loaded pipeline.
MiniMax-H3 (vllm.cpp)
The vllm-cpp backend — LocalAI's own C++ port of vLLM — also serves MiniMax-H3, which generates video and audio jointly. The clip comes back as an MP4 with a real AAC track rather than a silent render.
| Gallery model | Upstream checkpoint | Inputs | Output |
|---|---|---|---|
minimax-h3-fl2va-q4 |
MiniMaxAI/MiniMax-H3, Q4_K_M FL2VA partition |
text, optional start/end frame | video with generated audio |
local-ai models install minimax-h3-fl2va-q4
{{% notice warning %}} This is a large, slow model. The five weight files total roughly 40 GB, and generation was measured at about 176 seconds per denoise step at the default 1344x768 canvas on a 20-SM device — so the 50-step default is a multi-hour request, not a multi-second one. Nothing in the path imposes a deadline, but plan for a long-running HTTP call, and use a CUDA host. {{% /notice %}}
Ask for the sound
The model generates picture and sound from the same prompt, so a prompt that only describes what is seen produces room tone and ambience. To get speech, say that the character talks and put the words in the prompt:
It is TALKING to the camera: its mouth moves clearly in sync with its speech,
in a dry, deadpan tone.
It says, clearly and audibly: "Michael scheduled another all-hands.
It is about the printer. Again."
Audio: a single clear voice, close-miked, with quiet room tone underneath.
Geometry and clip length
The trained canvas is 1344x768 at 124 frames and 24 fps, about 5.2 seconds, and that is what the gallery entry defaults to. Two rules the engine enforces:
- The canvas is truncated onto a 32-pixel grid.
- The frame count sits on a 17n+5 grid (…, 90, 107, 124, 141, …). A count off the grid is rounded up, and LocalAI logs the value it actually rendered.
The trained clip range is roughly 124 to 362 frames (about 5 to 15 seconds).
Text-to-video with sound
curl http://localhost:8080/video \
-H "Content-Type: application/json" \
-d '{
"model": "minimax-h3-fl2va-q4",
"prompt": "A cyan llama mascot in a grey office chair, talking to the camera. It says, clearly and audibly: \"the printer is down again\". Audio: one clear close-miked voice.",
"num_frames": 124,
"step": 50,
"seed": 42
}'
First-frame conditioning
start_image pins the supplied image as frame 0 (H3's fl2va task); end_image pins the last frame. LocalAI converts the upload to the binary PPM at the exact output canvas that the engine requires, using ffmpeg, so PNG and JPEG uploads work. When no width/height is given, the canvas is derived from the image's aspect on a 768-pixel short edge.
curl http://localhost:8080/video \
-H "Content-Type: application/json" \
-d "{
\"model\": \"minimax-h3-fl2va-q4\",
\"prompt\": \"the subject turns toward the camera and starts speaking\",
\"start_image\": \"$(base64 --wrap=0 portrait.png)\"
}"
Partitions: what this checkpoint will and will not do
MiniMax-H3 ships as two DiT partitions, and the gallery entry installs FL2VA, which serves t2va (text only) and fl2va (first/last frame). Reference conditioning — a whole reference image, a reference clip, or reference audio — belongs to the separate Ref2VA checkpoint.
This matters because the mismatch does not fail cleanly upstream: a reference passed to an FL2VA DiT renders for hours and returns a coloured lattice over the frame. The backend refuses the combination up front instead, naming the partition. The community quantisations strip the release metadata and the two DiTs are byte-structurally identical, so the partition is declared in the model config (video_partition) rather than detected.
ffmpeg is required on the host
The engine writes frames and a WAV and composes the ffmpeg command line; LocalAI runs it. That process boundary is deliberate upstream, so the backend image ships no ffmpeg: install one on the host, or point options: [ffmpeg:/path/to/ffmpeg] at a binary. Without it, generation succeeds and the mux fails with a message saying so.
MiniMax-H3 model configuration
name: minimax-h3-fl2va-q4
backend: vllm-cpp
cuda: true
known_usecases:
- video
known_input_modalities:
- text
- image
known_output_modalities:
- video
options:
- video_encoder:minimax-h3/qwen3vl-32B-MiniMax-H3-Q4_K_M.gguf
- video_tokenizer:minimax-h3/tokenizer.json
- video_vae:minimax-h3/video_vae.safetensors
- video_vae_config:minimax-h3/video_vae_config.json
- audio_vae:minimax-h3/audio_vae.safetensors
- audio_vae_config:minimax-h3/audio_vae_config.json
- video_partition:fl2va
- video_device:cuda
- video_dequant_bf16:true
- video_width:1344
- video_height:768
- video_num_frames:124
parameters:
model: minimax-h3/MiniMax-H3-FL2VA-Q4_K_M.gguf
parameters.model is the DiT. H3 is a checkpoint set rather than one model directory, so the encoder, the tokenizer and the two VAEs are named in options. Relative paths resolve against the models directory.
Load options
| Option | Default | Description |
|---|---|---|
video_encoder |
— | H3 text encoder (GGUF or a bf16 shard directory). Required unless video_prompt_embeds is set |
video_tokenizer |
— | tokenizer.json for the encoder |
video_vae |
— | Video VAE weights (.safetensors). Required |
video_vae_config |
config.json beside the weights |
Carries latents_mean / latents_std and clip_length / token_drop; the decode is wrong without it |
audio_vae |
— | Audio VAE weights. Required |
audio_vae_config |
config.json beside the weights |
As above, for audio |
video_prompt_embeds |
— | Pre-computed f32 conditioning, as an alternative to an encoder |
video_partition |
fl2va |
fl2va or ref2va; must match the DiT you installed |
video_device |
cpu, or cuda when the config sets cuda: true |
cpu or cuda |
video_dequant_bf16 |
false |
Dequantise and stream the DiT as bf16; what the Q4_K_M GGUF arm wants |
video_fp4_resident |
false |
NVFP4 on CUDA: keep FP4 packed and use the Marlin W4A16 GEMM |
video_width / video_height |
1344 / 768 in the gallery entry | Default canvas when the request omits it |
video_num_frames |
124 in the gallery entry | Default clip length when the request omits it |
video_steps |
engine default (50) | Default denoise steps when the request omits it |
video_workdir |
a temporary directory | Where frame_%06d.ppm and audio.wav land. Set it to keep every run's frames |
video_crf |
18 | x264 CRF for the mux |
ffmpeg |
ffmpeg from PATH |
The mux binary |
Per-request parameters
The /video request's params object accepts string values. Unknown keys are rejected rather than ignored, so a typo does not cost you a multi-hour render of the wrong thing.
| Parameter | Description |
|---|---|
noise_aug |
Keyframe pinning strength; the default is 1.0 |
ref_image |
ref2va only: one whole reference image, as a binary PPM |
ref_video |
ref2va only: a directory of frame_%06d.ppm |
crf |
Per-request x264 CRF override |
negative_prompt, cfg_scale and fps have no MiniMax-H3 equivalent: H3 has no negative prompt or CFG scale, and it renders at a fixed frame rate that the audio track is synchronised to. Setting them is logged and ignored rather than silently honoured.
MiniMax-H3 troubleshooting
ffmpeg not found: install ffmpeg on the host or setoptions: [ffmpeg:<path>]. The frames and WAV are already rendered; only the mux failed.the FL2VA checkpoint serves t2va and fl2va only: you passed a reference image, clip or audio to the FL2VA DiT. Usestart_imagefor first-frame conditioning, or install a Ref2VA checkpoint.video_partition must be "fl2va" or "ref2va": the config declares something else.unknown params key:paramsaccepts only the four keys above.- Text inside the frame comes out malformed: this is the model's weakest area. Composite logos and signage in afterwards.
Error Responses
| Status Code | Description |
|---|---|
| 400 | Missing or invalid model or request parameters |
| 412 | The selected backend cannot run on the available hardware |
| 500 | Backend error during video generation |