llama.cpp-omni is a high-performance Omni multimodal inference engine built on llama.cpp.
- π First Full-Duplex Omni Streaming Engine β The first open-source C++ inference framework supporting full-duplex, omni-modal streaming video calls
- β‘ Lightweight & Efficient β Inherits llama.cpp's high-performance characteristics with GGUF quantization support and low memory footprint
- π Fully Ecosystem Compatible β Compatible with llama.cpp interfaces and ecosystem for seamless integration with existing toolchains
- π Cross-Platform Deployment β Supports Windows, Linux, and macOS, enabling efficient Omni model inference on consumer-grade hardware
- ποΈ End-to-End Voice Interaction β Supports the complete pipeline of streaming audio input, LLM inference, and TTS speech synthesis
MiniCPM-o 4.5 is a 9B-parameter on-device omni-modal large language model jointly developed by ModelBest and Tsinghua University, featuring powerful vision, speech, and full-duplex streaming capabilities.
Built on the MiniCPM-o 4.5 end-to-end omni-modal architecture, where modality encoders/decoders are densely connected to the LLM through hidden states. This design enables better information flow and control while fully leveraging the rich multimodal knowledge acquired during training.
llama.cpp-omni splits the original PyTorch model into multiple independent GGUF modules, each with specific responsibilities:
- VPM: Vision encoder based on SigLip2 architecture, responsible for encoding images into visual embeddings. Includes a Resampler module that compresses visual features into a fixed number of query tokens before projecting them into the LLM's hidden space.
- APM: Audio encoder based on Whisper architecture, responsible for encoding 16kHz audio into audio embeddings. Features AvgPool and Projector layers to project into the LLM's hidden space.
- LLM: Main language model based on Qwen3-8B, which receives visual and audio embeddings as input and generates text token sequences. Supports multiple quantization formats (F16/Q8_0/Q4_K_M).
- TTS: Text-to-speech model based on LLaMA architecture, which projects LLM hidden states through Projector Semantic and autoregressively generates audio token sequences.
- Token2Wav: Flow Matching-based vocoder that converts audio tokens into 24kHz waveform audio.
llama.cpp-omni implements a full-duplex streaming mechanism where input streams (video + audio) and output streams (speech + text) operate without blocking each other:
- Streaming Encoders: Transforms offline modality encoders into online streaming versions for real-time input processing. Audio is sliced into 1-second chunks for APM, while images are fed frame-by-frame to VPM.
- Time-Division Multiplexing (TDM): Within the LLM backbone, TDM divides parallel omni-modal streams into sequential information groups within periodic time slices, achieving millisecond-level input/output stream synchronization.
- Interleaved Speech Generation: The TTS module models text and speech tokens in an interleaved manner, supporting full-duplex speech generation where output can synchronize with new input in real-time while ensuring stability for long speech generation (>1 minute).
In duplex mode, the LLM continuously monitors incoming video and audio streams, deciding whether to speak proactively at 1Hz frequency. This high-frequency decision-making capability, combined with full-duplex features, enables proactive interactions such as spontaneous reminders and comments.
The core runtime pipeline of llama.cpp-omni consists of three stages:
-
Initialization (omni_init): Loads all GGUF models, initializes LLM/TTS/Token2Wav contexts, and configures simplex/duplex mode along with reference audio (for voice cloning).
-
Streaming Prefill (stream_prefill):
- When
index=0: Initializes System Prompt, including text system prompt and audio system prompt (reference audio embedding) - When
index>0: Processes user input β audio is encoded via APM, images via VPM, and embeddings are fed into LLM prefill - Supports high-resolution mode (max_slice_nums=2) and high-FPS mode (main image + stacked images)
- When
-
Streaming Decode (stream_decode):
- LLM autoregressively generates text tokens, entering speech generation upon
<|speak|>and switching to listening state upon<|listen|> - TTS projects LLM hidden states to generate audio tokens
- Token2Wav synthesizes WAV audio in real-time using a sliding window approach (28 tokens input, 25 tokens stride)
- All three modules execute in parallel via asynchronous queues, enabling streaming output
- LLM autoregressively generates text tokens, entering speech generation upon
| Stage | Latency | Notes |
|---|---|---|
| Time to First Token (TTFT) | < 550ms | First audio output |
| Prefill (vision + audio) | ~65ms | Audio-only ~21ms |
| Decode-LLM | ~38ms/token | 3 tokens ~115ms |
| TTS Generation | ~8.5ms/token | 25 tokens ~215ms |
| Token2Wav | RTF ~0.15x | 25 tokens β 1s audio ~150ms |
| Stage | Latency | Notes |
|---|---|---|
| Time to First Token (TTFT) | < 650ms | First audio output |
| Prefill (audio) | ~30ms | Audio-only |
| Decode-LLM | ~12ms/token | Metal accelerated |
| TTS Generation | ~10ms/token | Metal accelerated |
| Token2Wav (Token2Mel) | ~235ms/chunk | Metal accelerated |
| Token2Wav (Vocoder) | ~220ms/chunk | CPU (HiFiGAN) |
| Token2Wav Total | RTF ~0.47x | 28 tokens β 1s audio ~450ms |
| Configuration | LLM Quantization | Model Size | VRAM Estimate |
|---|---|---|---|
| Full Omni | F16 | ~18 GB | ~20 GB |
| Full Omni | Q8_0 | ~11 GB | ~13 GB |
| Full Omni | Q4_K_M | ~8 GB | ~9 GB |
| Vision Only | Q8_0 | ~9 GB | ~10 GB |
| Audio Only | Q8_0 | ~10 GB | ~12 GB |
| Configuration | LLM Quantization | Model Size | Unified Memory |
|---|---|---|---|
| Full Omni | F16 | ~15 GB | ~19 GB |
| Full Omni | Q8_0 | ~8.1 GB | ~12 GB |
| Full Omni | Q4_K_M | ~4.7 GB | ~8.5 GB |
Note: Apple Silicon uses unified memory architecture. Recommended: 16GB Mac for Q4_K_M/Q8_0, 32GB+ Mac for F16.
Model Files: Download MiniCPM-o 4.5 GGUF models with the following directory structure:
MiniCPM-o-4_5-gguf/
βββ MiniCPM-o-4_5-Q4_K_M.gguf # LLM (or F16/Q8_0)
βββ audio/
β βββ MiniCPM-o-4_5-audio-F16.gguf
βββ tts/
β βββ MiniCPM-o-4_5-tts-F16.gguf
β βββ MiniCPM-o-4_5-projector-F16.gguf
βββ token2wav-gguf/
β βββ encoder.gguf # ~144MB
β βββ flow_matching.gguf # ~437MB
β βββ flow_extra.gguf # ~13MB
β βββ hifigan2.gguf # ~79MB
β βββ prompt_cache.gguf # ~67MB
βββ vision/
βββ MiniCPM-o-4_5-vision-F16.gguf
# Configure
cmake -B build -DCMAKE_BUILD_TYPE=Release
# Build
cmake --build build --target llama-omni-server --target llama-omni-cli -jCMake will auto-detect and enable Metal (macOS) or CUDA (Linux with NVIDIA GPU).
# Basic usage (auto-detect all model paths from LLM path)
./build/bin/llama-omni-cli \
-m /path/to/MiniCPM-o-4_5-gguf/MiniCPM-o-4_5-Q4_K_M.gguf
# With custom reference audio (voice cloning)
./build/bin/llama-omni-cli \
-m /path/to/MiniCPM-o-4_5-gguf/MiniCPM-o-4_5-Q4_K_M.gguf \
--ref-audio /path/to/your_voice.wav
# Disable TTS (text-only output)
./build/bin/llama-omni-cli \
-m /path/to/MiniCPM-o-4_5-gguf/MiniCPM-o-4_5-F16.gguf \
--no-tts| Option | Description |
|---|---|
-m <path> |
Required. Path to LLM GGUF model |
--vision <path> |
Override vision model path |
--audio <path> |
Override audio model path |
--tts <path> |
Override TTS model path |
--projector <path> |
Override projector model path |
--ref-audio <path> |
Reference audio for voice cloning |
-c, --ctx-size <n> |
Context size (default: 4096) |
-ngl <n> |
Number of GPU layers (default: 99) |
--no-tts |
Disable TTS output |
--vision-batch-encode |
Encode same-size image slices in one batched pass (off by default; see below) |
--test <prefix> <n> |
Run test with audio files |
--bench-vision <img> |
Benchmark serial vs batched vision encoding on an image, then exit |
For high-resolution / high-refresh inputs, an image is split into one overview plus
many equally-sized slices, and each slice is encoded by the ViT. By default these
slices are encoded one at a time (serial). --vision-batch-encode instead packs
all same-size slices into a single batched ViT pass, which is significantly faster
when there are many slices.
- When to enable: large images / high-res / high-refresh modes, i.e. cases that produce many slices. On a 4821Γ2259 image this gives roughly 1.5β2.3Γ faster vision encoding (more slices β larger speedup).
- Why it's off by default: batched cuBLAS GEMM uses a different accumulation order than per-slice GEMM, so the embeddings are numerically very close but not bit-exact (avg diff ~1e-2). It also uses somewhat more VRAM. It is therefore opt-in rather than a universal default.
# Enable the optimization
./build/bin/llama-omni-cli \
-m /path/to/MiniCPM-o-4_5-gguf/MiniCPM-o-4_5-Q4_K_M.gguf \
--omni --vision-batch-encode
# Benchmark serial vs batched (prints a per-slice-count comparison table)
./build/bin/llama-omni-cli \
-m /path/to/MiniCPM-o-4_5-gguf/MiniCPM-o-4_5-Q4_K_M.gguf \
--bench-vision /path/to/large_image.pngProgrammatically the same switch is exposed via common_params.vpm_batch_encode
(applied in omni_init) and vision_set_batch_encode(ctx_vision, true).
Generated audio files are saved to tools/omni/output/:
tools/omni/output/
βββ round_000/
β βββ tts_wav/
β βββ wav_0.wav
β βββ wav_1.wav
β βββ ...
βββ round_001/
βββ tts_wav/
βββ wav_1000.wav
For an out-of-the-box, end-to-end omni video-call experience built on top of llama-omni-server, we recommend the Comni branch of the official demo:
π OpenBMB/MiniCPM-o-Demo @ Comni
It bundles a Python gateway + worker (which spawns and orchestrates llama-omni-server) and a desktop + mobile React frontend, supporting macOS (Metal), Linux (CUDA), and Windows (CUDA). Use it when you want a turnkey video-call demo without writing your own HTTP integration.
π‘ Don't want to compile? Pre-built one-click installers (Comni for Windows / macOS) are available on the llama.cpp-omni Releases page.
If you already have the GGUF weights from Prerequisites:
# 1. Build the C++ engine
git clone https://github.com/tc-mb/llama.cpp-omni.git
cd llama.cpp-omni && git checkout feat/web-demo \
&& cmake -B build -DCMAKE_BUILD_TYPE=Release \
&& cmake --build build --target llama-omni-server --target llama-omni-cli -j
cd ..
# 2. Set up the demo (Python venv + mobile frontend)
git clone https://github.com/OpenBMB/MiniCPM-o-Demo.git
cd MiniCPM-o-Demo && git checkout Comni
bash install.sh
( cd frontend/mobile && bun install && bun run --bun build:static ) # or `npm`
# 3. Configure (use absolute paths)
cp config.example.json config.json
# Edit config.json:
# "backend": "cpp"
# "cpp_backend.llamacpp_root" = absolute path to ../llama.cpp-omni
# "cpp_backend.model_dir" = absolute path to MiniCPM-o-4_5-gguf
# 4. Launch
CUDA_VISIBLE_DEVICES=0 bash start_all.sh
# 5. Open in browser
# https://localhost:8040/ (desktop)
# https://localhost:8040/mobile/ (mobile React)The detailed walkthrough below is the same content the demo repo's
README.md /
README_zh.md covers β kept here so you don't have to bounce between repos.
gateway.py :8040 (HTTPS) ββ
β HTTP / WS (internal)
worker.py :22440 + i GPU i ββ
β spawns + HTTP-calls
βΌ
llama-omni-server :19080 + i GPU i
/v1/stream/omni_init
/v1/stream/update_session_config
/v1/stream/prefill
/v1/stream/decode (SSE)
/v1/stream/break
1. Build llama-omni-server from this repo
git clone https://github.com/tc-mb/llama.cpp-omni.git
cd llama.cpp-omni
git checkout feat/web-demo
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --target llama-omni-server --target llama-omni-cli -jCMake auto-detects CUDA (Linux + NVIDIA) and Metal (macOS). After the build, build/bin/llama-omni-server is the binary worker.py will spawn β you do not need to start llama-omni-server yourself.
2. Install Python dependencies
git clone https://github.com/OpenBMB/MiniCPM-o-Demo.git
cd MiniCPM-o-Demo
git checkout Comni
bash install.sh # creates .venv/base/ + installs deps
# PYTHON=python3.11 bash install.sh # to use a different interpreterinstall.sh creates .venv/base/ (Python 3.10), upgrades pip, installs torch==2.8.0 + torchaudio==2.8.0, and finally installs requirements.txt. The C++ backend doesn't use PyTorch at runtime, but the worker is still a Python process so the venv is needed.
3. Configure config.json
Copy the template and set backend to cpp:
cp config.example.json config.json{
"backend": "cpp",
"cpp_backend": {
"llamacpp_root": "/abs/path/to/llama.cpp-omni",
"model_dir": "/abs/path/to/MiniCPM-o-4_5-gguf",
"llm_model": "MiniCPM-o-4_5-Q4_K_M.gguf",
"cpp_server_port": 19080,
"ctx_size": 8192,
"n_gpu_layers": 99
},
"audio": { "ref_audio_path": "assets/ref_audio/ref_minicpm_signature.wav",
"playback_delay_ms": 200 },
"service": {
"gateway_port": 8040,
"worker_base_port": 22440,
"num_workers": 1,
"max_queue_size": 1000,
"request_timeout": 300.0,
"data_dir": "data"
},
"duplex": { "pause_timeout": 60.0 }
}| Field | Purpose |
|---|---|
cpp_backend.llamacpp_root |
Absolute path to your llama.cpp-omni checkout. worker.py runs ${llamacpp_root}/build/bin/llama-omni-server and uses ${llamacpp_root}/tools/omni/output_<port>/ as the TTS WAV output dir |
cpp_backend.model_dir |
Absolute path to the GGUF directory (LLM + audio/ + tts/ + vision/ + token2wav-gguf/) |
cpp_backend.llm_model |
LLM filename inside model_dir. Pick the quantization you downloaded (Q4_K_M / Q8_0 / F16) |
cpp_backend.cpp_server_port |
HTTP port worker.py will start llama-omni-server on. Worker i uses cpp_server_port + i |
cpp_backend.ctx_size / n_gpu_layers |
Forwarded to llama-omni-server as --ctx-size / --n-gpu-layers |
4. Build the mobile frontend (one-time)
The /mobile/ route is served from static/mobile/, which is gitignored β it's the build output of the React + Vite project under frontend/mobile/:
cd frontend/mobile
bun install # or `npm install` (Node β₯ 20.19)
bun run --bun build:static # publishes to ../../static/mobile/
cd ../..See the demo repo's frontend/mobile/README.md for dev proxy / npm-only / hot-reload details.
5. Start the stack
CUDA_VISIBLE_DEVICES=0 bash start_all.shFirst boot loads all GGUF modules (VPM, APM, LLM, TTS, Token2Wav) and takes 10β60 s. The worker's /health returns worker_status: "idle" once omni_init finishes.
Then open:
https://localhost:8040/β desktop entry (Home / Omni / Audio-Duplex / Turnbased / Half-Duplex)https://localhost:8040/mobile/β mobile React frontendhttps://localhost:8040/mobile-omni/β mobile-adapted Omni page (DOM bridge over the desktopomni-app.js)
β οΈ Camera / microphone require HTTPS. The self-signed certs undercerts/work locally β accept the browser warning. Falling back tobash start_all.sh --httpwill only allow text input (browsers blockMediaDeviceson insecure origins).
pkill -f "gateway.py|worker.py|llama-omni-server"worker.py automatically restarts llama-omni-server after each session (full_reinit) to keep KV cache state clean across runs.
Set service.num_workers > 1 in config.json and pass the visible devices:
CUDA_VISIBLE_DEVICES=0,1 bash start_all.shEach worker is bound to its own GPU (via CUDA_VISIBLE_DEVICES) and spawns its own llama-omni-server on cpp_server_port + worker_index.
| Symptom | Likely cause |
|---|---|
Worker log: llama-omni-server not found |
cpp_backend.llamacpp_root is wrong, or cmake --build β¦ --target llama-omni-server was not run |
Worker /health stays at worker_status: "loading" for a long time |
omni_init is still loading GGUF modules. Check tmp/worker_<i>.log for lines tagged [CPP] |
WAV files appear under ${llamacpp_root}/tools/omni/output_<port>/round_XXX/ but the browser plays nothing |
The gateway is HTTP β many browsers block Audio / MediaDevices on insecure origins. Use the default HTTPS mode |
kv_cache_length keeps shrinking mid-conversation |
C++ side sliding-window pruning is kicking in. The desktop and mobile UIs expose a "Stop on KV pruning" toggle (default on) that ends the session cleanly when this happens |
For more details and Chinese documentation, see the demo repo: README.md / README_zh.md.
π This section is based on community integration experience.
This section documents the HTTP API call sequence for integrating llama-omni-server into your own application (e.g. a Tauri/Electron desktop app). The official CLI is a black box β if you want programmatic control, you need to call these endpoints directly.
This guide is based on real-world integration experience. Several critical details are not documented elsewhere.
./llama-omni-server \
--host 0.0.0.0 \
--port 9060 \
--model /path/to/MiniCPM-o-4_5-Q4_K_M.gguf \
-ngl 99 \
--ctx-size 8192 \
--repeat-penalty 1.05 \
--temp 0.7Poll GET /health until it returns 200 before proceeding. It typically takes 10β60 seconds.
# Wait for ready
curl http://localhost:9060/healthCall this once per application lifecycle. It loads all model modules, sets up voice cloning, and internally executes the index=0 prefill (system prompt initialization).
POST /v1/stream/omni_init
{
"media_type": 2,
"use_tts": true,
"duplex_mode": true,
"model_dir": "/path/to/MiniCPM-o-4_5-gguf",
"tts_bin_dir": "/path/to/MiniCPM-o-4_5-gguf/tts",
"tts_gpu_layers": 100,
"token2wav_device": "gpu:0",
"output_dir": "/path/to/output",
"voice_audio": "/path/to/reference_voice.wav"
}| Field | Description |
|---|---|
media_type |
2 = vision + audio (full omni) |
duplex_mode |
true enables full-duplex streaming |
voice_audio |
Reference WAV for voice cloning. Omit to use default voice |
output_dir |
Directory where TTS WAV files will be written |
Expected response:
{ "success": true, ... }
β οΈ omni_initinternally completesindex=0prefill. Do not send a separatecnt=0prefill after this call. Start your prefill counter at1.
After omni_init, enter a continuous loop. Each iteration sends 1 second of audio + 1 screenshot frame. The counter cnt increments by 1 each call and never resets within a session.
POST /v1/stream/prefill
{
"audio_path_prefix": "/path/to/audio_chunk.wav",
"img_path_prefix": "/path/to/screenshot.png",
"cnt": 1
}| Field | Description |
|---|---|
cnt |
Starts at 1, increments every call. 0 is reserved for omni_init |
audio_path_prefix |
1-second audio chunk (16kHz WAV). Send a silence chunk if mic is muted |
img_path_prefix |
Current screen frame. Can reuse last frame if no update |
β οΈ Always send an audio chunk, even when muted. Submitting a silence segment keeps the duplex loop rhythm intact. Skipping will cause timing drift.
Recommended loop cadence: 1000ms per iteration.
Call decode after each prefill. It triggers the LLM to generate a response and returns an SSE stream.
POST /v1/stream/decode
{
"debug_dir": "/path/to/output",
"stream": true
}SSE stream response format:
data: {"content": "Hello", "is_listen": false, "stop": false}
data: {"content": "!", "is_listen": false, "stop": false}
data: {"is_listen": true, "stop": false}
data: [DONE]
| Field | Description |
|---|---|
content |
Text token chunk. Empty string is possible, filter before display |
is_listen |
true = model has switched to listening state (stop playing audio) |
stop |
true = generation fully complete |
β οΈ The text field iscontent, nottext. This is inconsistent with standard OpenAI-compatible SSE format.
TTS WAV files are written incrementally to output_dir/round_XXX/tts_wav/. Watch this directory for new files and play them in order.
Use a filesystem watcher (e.g. notify in Rust) to detect new WAV files as they appear during decode.
output_dir/
βββ round_000/
β βββ tts_wav/
β βββ wav_0.wav
β βββ wav_1.wav
β βββ ...
βββ round_001/
βββ tts_wav/
βββ wav_1000.wav
β οΈ Mute your microphone input while playing back TTS audio to prevent echo feedback into the prefill loop.
start llama-omni-server
β
GET /health (poll until 200)
β
POST /v1/stream/omni_init (cnt=0 handled internally, start your counter at 1)
β
loop every ~1000ms:
POST /v1/stream/prefill { cnt: N, audio, image }
POST /v1/stream/decode β consume SSE β play WAV files from output_dir
N++
We welcome contributions. Start with the contributing guide, which links to the community task list grouped by difficulty, plus good first issues and open research areas.
If you want to pick something up, comment on the linked issue before writing code. For anything beyond a trivial fix we would rather agree on the approach first than review a large pull request that goes the wrong way.