Skip to content

Latest commit

 

History

History
577 lines (493 loc) · 32.2 KB

lfm3_audio.md

File metadata and controls

577 lines (493 loc) · 32.2 KB

LFM3-Audio

LFM3-Audio is Liquid AI's end-to-end speech and text model. audio.cpp runs its speech recognition (with a spoken-question chat mode), text-to-speech in 38 built-in voices and spoken chat (speech-to-speech) as the community family lfm3_audio.

It loads the llama.cpp-format GGUFs Liquid AI publishes in LiquidAI/LFM3-Audio-2B-GGUF, or the upstream LiquidAI/LFM3-Audio-2B safetensors directory.

Field Value
Family lfm3_audio
Status experimental
Tasks asr, tts, s2s
Modes offline; streaming for TTS and S2S
Languages ar, da, de, el, en, es, fi, fr, he, it, ja, ko, nl, pl, pt, sv, tr, zh
Audio input (ASR, S2S) WAV at any sample rate. Channels are averaged and the audio is resampled to 16 kHz with libsoxr (see Limitations)
Speech output (TTS, S2S) 24 kHz mono speech. S2S also returns the reply's text
Voices TTS: <language>_female and <language>_male for 19 languages (38 voices); american_english_female is the default. S2S replies in the checkpoint's own voice
Weights Liquid's GGUFs: Q4_0, Q4_K_M, Q8_0, F16, BF16. The upstream safetensors directory (BF16)
Backends tested CPU (x86 and Apple M3 Ultra), CUDA (NVIDIA H100), Metal (Apple M3 Ultra)
License LFM Open License v1.0: commercial use only for organizations under USD 10M yearly revenue (model licenses)

Architecture

  • Frontend: 128-bin log-mel at 16 kHz (25 ms window, 10 ms hop). A three-stage strided conv2d downsampler (8x in time) feeds a 17-layer LFM2 encoder (768 wide), and an MLP projector maps it to the backbone's width. One audio token is 80 ms of input.
  • LFM3 backbone: 24 layers, 2048 wide. 13 gated short-convolution layers (kernel 7), 6 full-attention and 5 sliding-window (256) attention layers with grouped-query attention, QK-norm, a per-head sigmoid gate and sandwich norms. The text head is tied to the token embedding.
  • Depth decoder: a 3-layer bidirectional transformer that predicts one audio frame of 12 codes (12.5 Hz) from one backbone state with MaskGIT, revealing 4 codebooks per pass in an order drawn per frame. A frame goes back into the backbone as the sum of its code embeddings.
  • LiquidCodec decoder: FSQ codes, an 8-layer LFM3 stack (causal, sliding window 30) over the frames repeated 6 times, then log-magnitude and phase and an ISTFT (n_fft 1280, hop 320) to 24 kHz audio. Offline decoding uses the streaming decoder in chunks of up to 128 frames.
  • ASR: the prompt is Perform ASR in <language>. (Perform ASR. with language=auto) and the audio as the user turn, decoded greedily. With chat=true the system turn is left out and the model answers the spoken input as text.
  • TTS: the prompt is Perform TTS in <language> using a <female|male> voice. and the text as the user turn. Each step after <|audio_start|> gives one 80 ms frame until a frame holds the end-of-audio code.
  • S2S: the system prompt Respond with interleaved text and audio. and the question as the user turn. The reply alternates 6 text tokens and 12 audio frames until <|text_end|>, keeps speaking until an end-of-audio frame, and ends at <|im_end|>.

Install

Liquid AI publishes each quantization as two llama.cpp-format GGUFs in LiquidAI/LFM3-Audio-2B-GGUF. The packages point at that repository, pinned to the linked revision:

File Contents
LFM3-Audio-2B-<quant>.gguf LFM3 backbone and text tokenizer
mmproj-LFM3-Audio-2B-<quant>.gguf Audio encoder and projector, depth decoder and codebook embeddings, LiquidCodec decoder (always F16)
Package Backbone mmproj Download
lfm3_audio_2b_q8_0 (default) Q8_0 Q8_0 2.2 GB
lfm3_audio_2b_f16 F16 F16 4.0 GB
lfm3_audio_2b_bf16 BF16 BF16 4.0 GB
lfm3_audio_2b_q4_k_m Q4_K_M Q8_0 1.4 GB
lfm3_audio_2b_q4_0 Q4_0 Q8_0 1.4 GB
python3 tools/model_manager_v2.py install lfm3_audio_2b_q8_0 --models-root models

All packages install into models/LFM3-Audio-2B-GGUF. --model takes a backbone GGUF, or the directory when it holds one backbone. The mmproj is mmproj-<backbone file> when present, else mmproj-LFM3-Audio-2B-Q8_0.gguf (which Liquid pairs with the 4-bit backbones), else the only mmproj there. The published GGUFs do not embed an audio.cpp model spec: it comes from model_specs/lfm3_audio.json, as for LFM2.5-Audio.

audio.cpp reads the tensors under the upstream checkpoint's names, and rebuilds what llama.cpp's converter turns into metadata or constants: the configs, the tokenizer (the backbone's vocabulary gives the same ids as the upstream tokenizer.json for all 128000 entries) and the codec's ISTFT window. In the BF16 GGUFs every backbone, encoder and depth-decoder tensor equals the upstream safetensors bit for bit; the codec decoder is F16 in every published GGUF.

Convert (optional)

The model also loads from the upstream directory as it is. Other types come from llama.cpp's tools, which write the published layout: convert_hf_to_gguf.py converts the upstream directory to the backbone GGUF (with --mmproj, to the mmproj), and llama-quantize makes other backbone types. Their LFM3-Audio support came with ggml-org/llama.cpp#31257. Only the five published types are tested.

Run

audiocpp_cli --task asr --family lfm3_audio \
  --model models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf --backend cuda \
  --audio speech.wav --language en

audiocpp_cli --task tts --family lfm3_audio \
  --model models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf --backend cuda \
  --text "The next train leaves in ten minutes." --voice-id british_english_male --out speech.wav

audiocpp_cli --task s2s --family lfm3_audio \
  --model models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf --backend cuda \
  --audio question.wav --out reply.wav --text-out reply.txt

From the upstream directory, pass it as --model models/LFM3-Audio-2B. Pass --family lfm3_audio to select this loader explicitly; automatic detection follows registry order and the safetensors layout is shared with other models. A server config entry needs "family": "lfm3_audio".

ASR

language (CLI --language) is one of the 18 codes above, or auto (the default) to let the model find the language. Audio up to audio_chunk_duration_sec (300 s by default) is transcribed in one pass; longer audio is split at Silero VAD pauses (audio_chunk_mode=auto), or into fixed chunks when the VAD model is missing. A chunk's transcript may run to max_tokens, by default 512 or 8 tokens per second of audio, whichever is more, and never past the 8192-step limit (Limitations). A transcript that reaches it is cut off there and a warning goes to stderr.

chat=true answers the spoken input instead of transcribing it, in one pass (no chunking); language has no effect on the answer, and the result names no language. enable_thinking=true (needs chat=true) lets the model think first and returns only the text after </think>. If thinking reaches max_tokens (512 for chat, 2048 with thinking), or the model ends its turn, without closing </think>, the answer is empty, as upstream, and a warning says so.

audiocpp_cli --task asr --family lfm3_audio \
  --model models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf --backend metal \
  --audio question.wav --request-option chat=true --request-option enable_thinking=true

TTS

--voice-id takes any of the 38 voices: arabic, danish, german, greek, british_english, american_english, finnish, french, hebrew, italian, japanese, korean, dutch, polish, portuguese, spanish, swedish, turkish or chinese, followed by _female or _male. Without a voice, language picks that language's female voice (en gives american_english_female; British English is reached only by voice id). Long text is split into chunks of about 200 characters (text_chunk_size, text_chunk_mode; japanese mode for Japanese and Chinese voices), and each chunk is its own turn of up to max_tokens frames (512, 41 s). Audio codes are sampled at temperature 1.0 from the top 128, with repetition penalty 1.1 over each codebook's last 4096 frames: the checkpoint's generation_config.json (clip.gen.audio.sampling.* in the published mmproj), which the model card recommends. temperature, top_k and repetition_penalty override it. Chunk i uses seed + i, so pass --seed for repeatable speech. Reference audio is rejected: there is no voice cloning.

With --mode streaming, each event carries stream_frames_per_event frames (1 by default, 80 ms). The codec is causal and keeps its state between frames, so the stream adds up to the offline speech for the same seed, up to the arithmetic of graphs of other sizes (Validation).

S2S

S2S answers a spoken question with a spoken reply and its text: the CLI prints the text and --text-out writes it. --text replaces the system prompt, which the checkpoint was trained with, so leave it out unless experimenting. S2S takes no voice and no language other than auto. The reply may run to max_tokens (1024) steps, text tokens and frames together. After the reply's text ends, at most max_audio_tail_frames frames (375, 30 s; 0 for no cap) are sampled before the turn is closed with an end-of-audio frame, because greedy audio (--temperature 0) can keep speaking. Text is greedy unless text_temperature is above 0.

With --mode streaming the question comes in as audio chunks (live PCM with --audio -); it is encoded and prefilled when the input ends, and the reply starts then. Each event carries one frame's audio (stream_frames_per_event) and the text written since the last event, in whole characters.

Server

The server takes the same paths and options:

{"models": [
  {"id": "lfm3-asr", "family": "lfm3_audio", "task": "asr", "mode": "offline",
   "path": "models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf"},
  {"id": "lfm3-tts", "family": "lfm3_audio", "task": "tts", "mode": "streaming",
   "path": "models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf"},
  {"id": "lfm3-s2s", "family": "lfm3_audio", "task": "s2s", "mode": "streaming",
   "path": "models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf"}]}
audiocpp_server --backend cuda --config server.json
curl http://127.0.0.1:8080/v1/audio/transcriptions -F model=lfm3-asr -F file=@speech.wav
curl http://127.0.0.1:8080/v1/audio/speech -H 'Content-Type: application/json' \
  -d '{"model": "lfm3-tts", "input": "The next train leaves in ten minutes.", "voice": "german_female"}' -o speech.wav
curl http://127.0.0.1:8080/v1/tasks/run -H 'Content-Type: application/json' \
  -d '{"model": "lfm3-s2s", "request": {"audio": "/path/to/question.wav"}}'

The TTS entry streams, and a request without stream_format still gets the whole WAV. With "stream_format": "sse", speech comes back as it is decoded: speech.audio.delta events of base64 16-bit PCM at 24 kHz, which add up to the WAV, then speech.audio.done with the time to the first audio (server API).

curl -N http://127.0.0.1:8080/v1/audio/speech -H 'Content-Type: application/json' \
  -d '{"model": "lfm3-tts", "input": "The next train leaves in ten minutes.", "voice": "german_female", "stream_format": "sse"}'

/v1/tasks/run returns the S2S reply's text and its audio as base64 WAV. /v1/tasks/stream with "stream_format": "sse" streams it from a streaming entry. See the server README.

The live route, /v1/audio/speech/live, takes the question as chunked raw PCM for a streaming entry, and the reply starts when the input ends. Each live request is a first turn. Without input the reply uses the default system prompt; an input replaces it, as --text does.

ffmpeg -i question.wav -ar 16000 -ac 1 -f s16le - \
  | curl -N -X POST -H 'Expect:' -T - \
      'http://127.0.0.1:8080/v1/audio/speech/live?model=lfm3-s2s&sample_rate=16000&channels=1&sample_format=s16le&return_text=true'

It returns the reply's audio as server-sent events and, with return_text=true, its text (server API). The query also takes the request options seed, temperature, top_k, repetition_penalty, text_temperature, text_top_k and max_tokens. The route does not forward max_audio_tail_frames, stream_frames_per_event or return_codes, so those keep their defaults; use /v1/tasks/stream for the reply artifact. The first audio of a reply comes after encoding the whole question, the prompt, the first text block and the first frame.

WebUI

The WebUI lists LFM3-Audio under ASR, TTS and speech-to-speech, each entry reading models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf from the lfm3_audio_2b_q8_0 package, which the WebUI can download. ASR has the chat and enable_thinking options, TTS offers the 38 voices, and speech-to-speech has no text box or language, so replies use the default system prompt.

The speech-to-speech page holds a conversation: each Run sends the recorded or chosen question to /v1/tasks/stream with the earlier turns, as Conversations describes, plays the reply as it streams in and adds the turn to a list, and New conversation starts over. With --ui-management the page loads the entry in streaming mode itself. With a config file, live playback needs the entry in "mode": "streaming"; an offline entry gives each reply when it is done.

Conversations

audio.cpp keeps no conversation state: the client sends the earlier turns with each new question, as the request's input artifacts, in order. For each earlier turn, its question and then its reply:

Artifact id Kind Payload
lfm3_audio.question custom The question's audio as a WAV file
lfm3_audio.reply acoustic_tokens The reply artifact that turn's result returned, payload and meta unchanged

With return_codes=true, an S2S result, offline or streamed, also returns its reply as an lfm3_audio.reply artifact. The payload is little-endian int32 values, one step after another: a text token is its id, and an audio frame is -1 followed by its 12 codes. It holds every step, the end-of-audio frame included, and goes back into the prompt verbatim, which equals continuing the upstream model's KV cache. A reply that was cut off (ended=false) gets an end-of-audio frame and <|im_end|> appended when replayed. Its meta:

Key Value
format lfm3_audio.reply/1
codebooks, audio_vocab_size, text_vocab_size 12, 2017 (2016 codes and end-of-audio), 128000
steps Text tokens and frames in the payload
ended true when the reply ended; false when max_tokens, or a stream finished early, cut it off

A server request sends the history in its artifacts array; each artifact takes a base64 payload or a path:

{"model": "lfm3-s2s", "request": {
  "audio": "/path/to/turn2.wav",
  "options": {"return_codes": true},
  "artifacts": [
    {"id": "lfm3_audio.question", "kind": "custom", "path": "/path/to/turn1.wav"},
    {"id": "lfm3_audio.reply", "kind": "acoustic_tokens", "payload": "<turn 1's base64>",
     "meta": {"format": "lfm3_audio.reply/1", "codebooks": "12", "audio_vocab_size": "2017",
              "text_vocab_size": "128000", "steps": "<turn 1's steps>", "ended": "<turn 1's ended>"}}
  ]}}

The CLI's --request-sequence JSON takes the same artifacts array, and the C API takes them with audiocpp_request_add_artifact and audiocpp_request_set_artifact_meta, as for LFM2.5-Audio. Artifacts out of turn, with other ids or meta, from another format version, or sent to ASR or TTS are request errors (HTTP 400).

The prompt and max_tokens may take 8192 steps together (an 80 ms audio token, a text token or a frame is one step). Past that the turn fails with a CapacityError and the client leaves out the oldest turns. Every turn encodes all its questions and prefills the whole conversation again (in 256-step blocks), so later turns take longer.

Request Options (use with --request-option)

Option Task Default Meaning
language all auto ASR: the spoken language, or auto. TTS: picks that language's female voice when no voice is given. S2S: only auto.
max_tokens all ASR: max(512, 8 per second); chat 512, 2048 with thinking. TTS: 512. S2S: 1024 ASR: text tokens per chunk. TTS: 80 ms frames per text chunk. S2S: text tokens and frames of the reply together.
audio_chunk_mode ASR auto auto, fixed, vad or none.
audio_chunk_duration_sec ASR 300 Longest chunk in seconds, at least 1; never above lfm3_audio.max_pass_duration_sec.
chat ASR false Answer the spoken input as text instead of transcribing it.
enable_thinking ASR false With chat=true, think first; only the text after </think> is returned.
temperature TTS, S2S 1.0 Audio code temperature; 0 picks the most likely code. The default is the checkpoint's.
top_k TTS, S2S 128 Sample audio codes from the k most likely; 0 keeps all. The default is the checkpoint's.
repetition_penalty TTS, S2S 1.1 Audio repetition penalty over the last 4096 frames of each codebook; 1 disables it. The default and the window are the checkpoint's.
text_temperature ASR, S2S 0 Text token temperature; 0 is greedy.
text_top_k ASR, S2S 0 Sample text tokens from the k most likely; 0 keeps all.
seed all Random Sampling seed; TTS text chunk i uses seed + i.
text_chunk_mode TTS japanese for Japanese and Chinese voices, else default default, tag_aware, japanese or endline.
text_chunk_size TTS 200 Target characters per text chunk.
stream_frames_per_event TTS, S2S streaming 1 Audio frames (80 ms) per event.
max_audio_tail_frames S2S 375 Most frames after the reply's text ends before the turn is closed with an end-of-audio frame; 0 disables the cap.
return_codes S2S false Also return the lfm3_audio.reply artifact for the next turn.

Each task rejects the options it does not take.

Session Options (use with --session-option)

Option Default Meaning
lfm3_audio.weight_type native native keeps the file's types (BF16 is loaded as F16 where the backend has no fast BF16 matmul: Metal and the arm64 CPU). f16 converts the weights to F16 at load. f32 loads them as F32 and also uses F32 KV caches and explicit attention: the parity mode.
lfm3_audio.cpu_repack true On the CPU, repack matmul weights for ggml's extra buffer kernels. Other backends ignore it.
lfm3_audio.vad_model_path assets/framework/models/silero_vad VAD model (any built-in VAD family) used by audio_chunk_mode auto and vad.
lfm3_audio.max_pass_duration_sec 300 Most audio, in seconds, the encoder takes in one pass (an ASR chunk, the S2S question or an earlier question); longer audio is rejected before it is encoded.

Developers

Parity mode and stage dumps

--session-option lfm3_audio.weight_type=f32 runs every weight, KV cache and attention in F32, the mode the stage checks compare against the fp32 reference. These environment variables are for parity work:

Variable Effect
LFM3_AUDIO_DUMP_DIR=<dir> Write each stage to <dir>/<name>.npy (float32 or int32, C order).
LFM3_AUDIO_DUMP_FRAMES=<n> Frames whose per-pass depth logits are dumped (default 4).
LFM3_AUDIO_MASKGIT_ORDERS=<file.npy> Int32 [frames, 12]; each row, a permutation of 0..11, replaces the MaskGIT order of that frame in every generation. With greedy sampling this reproduces a reference run exactly.

Dumps: wave16k, features, downsample, encoder, projector, prompt_ids, prefill_hidden, prefill_logits, steps ([S, 13]: the token or -1, then 12 codes or -1), maskgit_orders, depth_logits_f<f>_p<p>, codes, codec_spectrum and audio24k.

Fixtures

tools/community_models/lfm3_audio/make_fixtures.py runs the upstream model in fp32 on CUDA (needs torch, transformers, librosa) and writes the reference arrays and manifest.json, with the bf16-vs-fp32 noise floor of each stage:

python3 tools/community_models/lfm3_audio/make_fixtures.py --model models/LFM3-Audio-2B \
  --assets assets/resources --out models/LFM3-Audio-2B/fixtures

The fixtures come from the model weights. Keep them, and every dump, out of git.

Tests

The 13 unit tests (tests/community_models/lfm3_audio/) build a tiny synthetic package and need no weights. They build with the unit tests (-DENGINE_BUILD_TESTS=ON):

ctest --test-dir build -R lfm3_audio

The real-weight tests test_lfm3_audio_parity, test_lfm3_audio_asr, test_lfm3_audio_tts and test_lfm3_audio_s2s build with -DENGINE_BUILD_MODEL_TESTS=ON. They read every expected value from the fixture directory at run time and exit 125 (skipped) without the model or the fixtures:

build/bin/test_lfm3_audio_parity --model models/LFM3-Audio-2B \
  --fixtures models/LFM3-Audio-2B/fixtures --backend cpu --weight-type f32 --threads 16
build/bin/test_lfm3_audio_asr --model models/LFM3-Audio-2B-GGUF/LFM3-Audio-2B-Q8_0.gguf \
  --fixtures models/LFM3-Audio-2B/fixtures --backend best --weight-type native

--model takes the upstream directory or a GGUF (a published backbone GGUF brings its mmproj), --backend is cpu or best, and --weight-type is f32 (the default) or native. The tolerance follows the weights: F32 on the CPU 1e-5 rel per stage (3e-5 for the encoder stages run on the host's features, 1e-4 for the 418 s clip and the codec waveform) with identical greedy output; F32 on a GPU 5e-3 (waveform 1e-2), where greedy output may differ only at a near-tie; BF16 and F16 max(2e-2, 2x the stage's noise floor), with the same argmax and the same ASR transcript; Q8_0 max(5e-2, 3x the floor); the 4-bit types (Q4_K_M, Q4_0) are reported only. The tolerance is picked from the file name, so keep the quantization in it.

lfm3_audio_tokenizer_test --model <upstream dir> --fixtures <dir> --gguf <backbone GGUF> also checks a published backbone's vocabulary against tokenizer.json, entry by entry.

Validation

Real-weight runs on 2026-10-27 with the public checkpoint: CPU and CUDA on an NVIDIA H100 host, CPU and Metal on an Apple M3 Ultra. F32 rows load the upstream safetensors directory with weight_type=f32; the others load Liquid's GGUFs with their own types. rel is the relative L2 error against the fp32 reference. Stages: the encoder output (projector), the last-position logits of an ASR prompt and of a 418 s one-pass prompt, the depth decoder's logits on the reference's inputs (40 frames, 1440 codes), and the codec waveform for real and random codes.

Config Projector ASR logits 418 s logits Depth logits Depth argmax Codec wave (TTS / random)
CPU F32 (x86) 1.5e-6 2.1e-6 4.8e-6 3.5e-7 1440/1440 2.7e-6 / 2.4e-5
CPU F32 (M3 Ultra) 1.5e-6 1.9e-6 4.0e-6 3.6e-7 1440/1440 2.6e-6 / 2.4e-5
CPU BF16 (x86) 3.9e-3 6.0e-3 2.0e-3 1.6e-3 1425/1440 3.0e-4 / 7.0e-4
CPU BF16 as F16 (M3 Ultra) 2.1e-3 2.8e-3 1.6e-3 8.5e-4 1434/1440 2.7e-3 / 1.5e-2
CUDA F32 4.9e-4 3.7e-4 4.4e-4 6.0e-4 1437/1440 1.1e-3 / 8.1e-3
CUDA BF16 4.1e-3 2.1e-3 1.3e-3 1.5e-3 1435/1440 1.3e-3 / 8.8e-3
CUDA F16 5.0e-4 5.6e-4 1.0e-3 2.5e-4 1438/1440 1.3e-3 / 8.8e-3
CUDA Q8_0 2.9e-2 1.3e-2 8.6e-3 8.6e-3 1378/1440 1.3e-3 / 8.8e-3
CUDA Q4_K_M 2.9e-2 8.1e-2 0.42 8.6e-3 1378/1440 1.3e-3 / 8.8e-3
CUDA Q4_0 2.9e-2 0.13 0.78 8.6e-3 1378/1440 1.3e-3 / 8.8e-3
Metal F32 4.8e-4 3.0e-4 3.4e-4 1.8e-4 1438/1440 7.2e-4 / 7.6e-3
Metal BF16 = F16 4.6e-4 3.6e-4 5.1e-4 1.8e-4 1439/1440 7.9e-4 / 6.7e-3
Metal Q8_0 1.7e-2 8.8e-3 8.6e-3 4.8e-3 1403/1440 7.9e-4 / 6.7e-3
Metal Q4_K_M 1.7e-2 8.0e-2 0.40 4.8e-3 1403/1440 7.9e-4 / 6.7e-3
Metal Q4_0 1.7e-2 0.13 0.62 4.8e-3 1403/1440 7.9e-4 / 6.7e-3

Metal has no BF16 matmul and the arm64 CPU has no SIMD BF16 dot product, so the BF16 GGUF loads as F16 on both. On Metal it gives the same numbers as the F16 GGUF. Every published GGUF keeps the codec at F16, and the Q4_K_M and Q4_0 packages share the Q8_0 mmproj, so the encoder, depth decoder and codec columns repeat across those rows. The published BF16 GGUF with weight_type=f32 gives the CUDA F32 row's numbers except the codec (1.1e-3 / 8.9e-3).

  • ASR: the short-clip transcripts, with language=en and auto, equal the fp32 reference's with F32, BF16, F16 and Q8_0 on every backend; the 4-bit backbones change words. On a 418 s English recording (language=auto), WER against a whisper-large-v3 transcript is 0.22% with the default VAD-split passes and in one pass, for F32 on CUDA and the M3 Ultra's CPU and every published type on CUDA, Metal and both CPUs, except Q4_0: 0.07% with VAD passes and 0.29% in one pass.
  • Thinking chat: identical tokens to the reference on 4 clips with F32 on the H100 host's CPU and on CUDA and with the BF16 GGUF on CUDA.
  • TTS: with injected MaskGIT orders and greedy codes, F32 on the CPU and on Metal gives the reference's frames exactly; the other configs diverge only where the top two codes are within 1e-2, or are reported only (quantized). Text read back by ASR (5 sentences) gives 0% WER on every config, and all 38 voices speak and end. Streamed speech differs from offline by at most 2.5e-3 rel.
  • S2S: F32 on either CPU gives the reference's greedy reply step for step (232 steps, and a reply cut at 96). Streamed replies have the same text and reply artifact as offline in every config, and audio within 2.6e-3 rel (CUDA F16: 1.1e-2). For a second turn, the replayed prompt matches the reference token for token, and its first logits are within 7.0e-7 rel on the CPUs in F32, 1.5e-4 to 2.1e-4 on GPUs in F32 and F16, 9.3e-4 to 1.4e-3 with BF16 and 4.6e-3 to 7.2e-3 in Q8_0. The argmax is the reference's in every config but one 4-bit case (CUDA Q4_K_M after the cut reply).

Performance

Liquid's Q8_0 GGUFs with the checkpoint's sampling defaults, seed 42 and --threads 8, on 2026-10-27. Offline numbers are the median of 3 timed runs after a warm-up; streaming numbers are measured by the client, the median of 3 (live route: 5). TTS reads 15 English sentences and S2S answers 10 spoken questions. RTF is processing time over audio duration. The hosts were measured separately, and these methods differ, so do not compare the two columns:

  • Live route input: a 2.1 s question on the H100, the 7.5 s clip on the M3 Ultra.
  • TTS RTF: the median of the 15 per-sentence RTFs on the H100, total time over total audio on the M3 Ultra.
  • Load time: the median of 7 ASR processes on the H100; on the M3 Ultra, the median of 3 runs on a 1 s clip after a first.
  • S2S: each host asked its own 10 questions.
  • Peak memory: the device's GPU memory on the H100, the process footprint on the M3 Ultra.
Metric H100 (CUDA) M3 Ultra (Metal)
ASR RTF, 7.5 s / 418 s clip 0.015 / 0.019 0.029 / 0.041
ASR WER, 418 s clip, auto / en 0.07% / 4.31% 0.14% / 4.38%
TTS RTF 0.063 0.138
TTS time to first audio 15 ms 37 ms
S2S time to first audio, turn 1 / turn 2 48 ms / 90 ms 83 ms / 167 ms
S2S streaming RTF 0.090 0.182
Live route, first audio after the input ends 48 ms 154 ms
Model files / load time 2083 MB / 2.7 s 2083 MB / 1.1 s
Peak memory, ASR 7.5 s / ASR 418 s / TTS 2683 / 4719 / 2751 MiB 3084 / 3594 / 3172 MB

The 418 s clip's reference is a whisper-large-v3 transcript; the ASR RTF on it is with language=en. These WERs fold apostrophes and dashes (and, on the M3 Ultra, spell out numbers) where the Validation numbers do not, so they differ from its 0.22%. Turn 2 is slower because it encodes and prefills the whole conversation again.

Limitations

  • ASR is offline only; TTS and S2S also stream. The model card's live ASR (Perform live ASR., about 0.96 s chunks transcribed as they arrive) is not implemented.
  • TTS speaks with the 38 built-in voices only; there is no voice cloning. S2S replies in the checkpoint's voice and takes no voice.
  • Input at other sample rates is resampled with libsoxr, as the upstream processor does through librosa. audio.cpp loads libsoxr at run time; where it is not found, as on a stock macOS, it falls back to a sinc resampler that differs by about 6% in the top mel bands and can change words, with a warning under --log (on one 24 kHz stereo clip it gave the same transcript as libsoxr with every type but 4-bit). Install it (brew install libsoxr, apt install libsoxr0), or send 16 kHz audio.
  • On CUDA, the short convolutions do not use ssm_conv, which aborts on the backbone's kernel width 7 in ggml-cuda: passes over several steps (prefill, the encoder, the codec) run them as shifted row views, and a decode step as one multiply and a row sum. The CPU and Metal use ssm_conv.
  • ASR takes no request text: --text is ignored, so the model card's uses that pair the audio with a typed instruction (translation, questions about the audio, tool calls) are not available.
  • A request's prompt and output share at most 8192 steps, audio.cpp's limit, a quarter of the model card's 32K-token context.
  • The 4-bit backbones lose accuracy: the 418 s prompt's logits are 0.40-0.42 rel off with Q4_K_M and 0.62-0.78 with Q4_0 (0.009 with Q8_0), and the short clip's transcript differs ("Hallway English Council" for "Whole Life English Council"). Their packages use the Q8_0 mmproj, so the encoder, depth decoder and codec match the Q8_0 package. Use Q8_0 or better.
  • The published GGUFs store the codec decoder in F16 where the upstream checkpoint has BF16 and F32 tensors. In F32 mode the published BF16 GGUF gives the upstream weights everywhere else, and its codec waveform is as close to the reference as the upstream directory's on CUDA (1.1e-3 rel for TTS codes).
  • Every turn of a conversation encodes all its questions and prefills the whole conversation again; there is no cache across turns.
  • Thinking chat (enable_thinking) gives the reference's tokens exactly on 4 clips with F32 on the H100 host's CPU and on CUDA and with the published BF16 GGUF on CUDA; Q8_0 diverges partway through, at gaps of 0.007-0.15. Over HTTP, an empty answer carries no flag; the warning goes only to the server log.
  • With language=en, ASR of the 418 s clip repeats the passage before the end of its last VAD pass (1451 words against 1392; 4.3-4.4% WER in Performance, against 0.07-0.14% with the default auto), with Q8_0 and F16 on CUDA and Metal. This is the model, not the port: on that 122 s pass the upstream model in fp32 with greedy decoding gives the same 562 tokens as audio.cpp with F32 on the CPU and with Q8_0 on CUDA. Where the passage ends, the model goes on with "We" (logit 18.1) instead of ending its turn (12.3). The pass has speech up to the file's last sample. language=auto avoids it on this clip.
  • A long S2S reply can stop speaking before its text ends: in the H100 performance runs, one story reply's audio stopped after 16.6 s with 59 of its 108 words spoken. It is rare, and the upstream model does it too: on the same story question with the checkpoint's sampling defaults, 1 of 40 upstream replies (bf16) and 0 of 40 audio.cpp replies (Q8_0) had a text-vs-audio WER above 15% by whisper-large-v3.
  • TTS misreads some numbers ("48 dollars" was heard as "$40" on the H100; "1,250" was garbled on the M3 Ultra) that LFM2.5-Audio reads correctly.
  • LFM3-Audio costs more than LFM2.5-Audio: the Q8_0 package is 2083 MB against 1738 MB, it loads in 1.1-2.7 s against 0.5-0.7 s, and it takes 1.2-2x the peak memory (H100, 418 s ASR: 4719 against 2405 MiB GPU memory; M3 Ultra: 3594 against 2208 MB).