Interfaze

logo

we're hiring

pricing

help

docs

blog

sign in

All models

LFM3 Omni 2B

LFM3 Omni 2B by LiquidAI, a any-to-any model with multimodal capabilities. Understand and compare multimodal features, benchmarks, and capabilities.

Comparison

FeatureLFM3 Omni 2BInterfaze
Input Modalities

text, image, video, audio

image, text, audio, video, document

Native OCRNoYes
Long Document ProcessingNoYes
Language Support

19 partial

162+

Native Speech-to-TextNoYes
Native Object DetectionNoYes
Guardrail ControlsNoYes
Context Input Size

32K

1M

Tool CallingYes

Tool calling supported + built in browser, code execution and web search

Scaling

FeatureLFM3 Omni 2BInterfaze
Scaling

Self-hosted/Provider-hosted with quantization

Unlimited

Try InterfazeRead the Docs

View model card on Hugging Face

LFM3-Omni-2B is Liquid AI's first omni-modal model in the LFM3 family: a single end-to-end model that understands text, images, video and audio, and answers in text and speech. It is built for on-device use: one model replaces a separate ASR model, vision-language model, LLM and TTS model.

  • Any input, text or speech output: ask about a photo by voice, transcribe or translate speech, chat out loud, or ask questions about a video and its soundtrack together.
  • Watches and listens together: for videos with sound, the model sees and hears them at the same time, so it can answer questions that need both.
  • Small enough for devices: at 2.2B parameters, it is designed to run efficiently on phones, laptops and other edge hardware.

Find more information about the LFM3 family in our release blog post.

Model details

The LFM3 family includes base, text, vision, audio, omni, and decision models.

This is a non-reasoning model. enable_thinking=True is not supported.

Inference

Choose a checkpoint variant for your inference framework and hardware.

Sampling parameters

  • Text output: greedy decoding works well.
  • Speech or interleaved output: use audio_temperature=0.4, audio_repetition_penalty=1.2

Generation modes (optional)

To force a generation mode, pass the optional generation_mode argument to model.generate.

By default, the model selects the output modality based on the system prompt and input modality, typically matching the input modality. To encourage a specific output modality, append the corresponding instructions to the end of your system prompt.

Transformers quick start

Install the dependencies, then load the model and processor with the checkpoint's custom code. The codec implementation is included in this repository, and the processor loads its weights from audio_tokenizer/.

pip install "transformers>=5.20.2" torch torchvision torchcodec accelerate pillow librosa soundfile
import torch
import soundfile as sf

from transformers import AutoModelForMultimodalLM, AutoProcessor

model_id = "LiquidAI/LFM3-Omni-2B"
device = "cuda" if torch.cuda.is_available() else "cpu"
dtype = torch.bfloat16 if device == "cuda" else torch.float32

model = AutoModelForMultimodalLM.from_pretrained(
    model_id, dtype=dtype, device_map=device, trust_remote_code=True
).eval()
processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
codec = processor.audio_tokenizer.to(device=device).eval()

LFM3-Omni uses a ChatML-like format, which processor.apply_chat_template() renders for you, generated from the checkpoint's chat template:

<|startoftext|><|im_start|>system
Respond with text.<|im_end|>
<|im_start|>user
Name a famous bridge in San Francisco.<|im_end|>
<|im_start|>assistant
<think></think>The Golden Gate Bridge.<|im_end|>
<|im_start|>user
When did it open?<|im_end|>
<|im_start|>assistant
<think></think>

The system turn is optional, and every assistant turn opens with an empty <think></think>. Each image, video or audio clip is wrapped in <|vision_start|> / <|vision_end|>, which the template adds. Here is an image and a spoken question:

<|startoftext|><|im_start|>user
<|vision_start|><|image|><|vision_end|><|vision_start|><|audio|><|vision_end|>Answer in one sentence.<|im_end|>
<|im_start|>assistant
<think></think>

Usage examples

The examples below reuse the model, codec, and processor loaded above. Replace audio.wav, question.wav, recording.wav, photo.jpg, or clip.mp4 with your own files. Audio files are resampled to 16 kHz mono on load.

To use the model for transcription, use the system prompt Perform ASR in <language>., where <language> is the English name of the language.

Below, we transcribe a .wav file containing English speech.

conversation = [
    {"role": "system", "content": "Perform ASR in English."},
    {"role": "user", "content": [{"type": "audio", "audio": "audio.wav"}]},
]

inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(device)
with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=1024, generation_mode="text")
text = processor.decode(
    outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
    skip_special_tokens=True,
)
print(text)

For speech synthesis, use the system prompt Perform TTS in <language> using a <gender> voice.. Use one of the speech languages listed in Model details and a female or male voice. For English, say American English or British English. Pass generation_mode="audio" so the answer opens with <|audio_start|>.

conversation = [
    {"role": "system", "content": "Perform TTS in American English using a female voice."},
    {"role": "user", "content": "The quick brown fox jumps over the lazy dog"},
]
inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(device)
with torch.no_grad():
    outputs = model.generate(
        **inputs,
        max_new_tokens=256,
        generation_mode="audio",
        audio_temperature=0.4,
        audio_repetition_penalty=1.2,
    )
    waveform = codec.decode(
        outputs.audio_codes[:1, :, :outputs.audio_lengths[0]]
    ).audio_values[0]  # 24 kHz

sf.write("tts.wav", waveform.squeeze(0).float().cpu().numpy(), 24_000)

In this mode, the model will respond to the user in both speech and text. The recommended system prompt is Respond with interleaved text and audio.. User input can be either text or audio, and we pass generation_mode="interleaved" so the answer opens with <|mixed_start|>.

The assistant responds in a conversational style with interleaved text and speech. This example saves the complete response.

conversation = [
    {"role": "system", "content": "Respond with interleaved text and audio."},
    {"role": "user", "content": [{"type": "audio", "audio": "question.wav"}]},
]
inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(device)
with torch.no_grad():
    outputs = model.generate(
        **inputs,
        max_new_tokens=1024,
        generation_mode="interleaved",
        audio_temperature=0.4,
        audio_repetition_penalty=1.2,
    )

    text = processor.decode(
        outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
        skip_special_tokens=True,
    )
    waveform = codec.decode(
        outputs.audio_codes[:1, :, :outputs.audio_lengths[0]]
    ).audio_values[0]  # 24 kHz
print(text)
sf.write("reply.wav", waveform.squeeze(0).float().cpu().numpy(), 24_000)

For a text-only answer, no specific system prompt is needed. Respond with text. makes it explicit. Pass generation_mode="text". LFM3-Omni-2B is a non-reasoning model: every answer opens with an empty <think></think>.

conversation = [
    {"role": "user", "content": [{"type": "audio", "audio": "question.wav"}]},
]
inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(device)
with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=1024, generation_mode="text")
    text = processor.decode(
        outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
        skip_special_tokens=True,
    )
print(text)

Include the recording and a question in the user turn. Questions can address speech, music, background sounds, or events. No specific system prompt is required. To ground answers in the recording, use Answer questions about the provided audio clip. Base every answer only on what can be heard..

conversation = [
    {
        "role": "system",
        "content": "Answer questions about the provided audio clip. Base every answer only on what can be heard.",
    },
    {
        "role": "user",
        "content": [
            {"type": "audio", "audio": "recording.wav"},
            {"type": "text", "text": "What is happening in this recording?"},
        ],
    },
]
inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(device)

with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=1024, generation_mode="text")

print(processor.decode(
    outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
    skip_special_tokens=True,
))

For multiple-choice questions, include the choices in the text part and end it with Please choose the answer from the options.

Use a system prompt that specifies the source language, target language, and output format:

  • Speech to text: Please translate the following spoken {source} into {target} text.

Replace {source} and {target} with the English names of the source and target languages. For text input, set the user message's content to the text to translate. Translation to or from English generally works best.

Include the image and a question in the user turn. To ground answers in the image, use the system prompt Answer questions about the provided image. Base every answer only on what can be seen.. For captions, use Describe the provided image.. Images are processed at their native resolution, up to 2,048 tokens per image.

conversation = [
    {
        "role": "system",
        "content": "Answer questions about the provided image. Base every answer only on what can be seen.",
    },
    {
        "role": "user",
        "content": [
            {"type": "image", "image": "photo.jpg"},
            {"type": "text", "text": "What is written on the sign?"},
        ],
    },
]
inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(device)

with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=1024, generation_mode="text")

print(processor.decode(
    outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
    skip_special_tokens=True,
))

The question can be asked by voice, with the recording in place of the text. Add Respond with text. to the system prompt for a text answer, or follow the voice chat example with Respond with interleaved text and audio. for a spoken one.

conversation = [
    {
        "role": "system",
        "content": "Answer questions about the provided image. Base every answer only on what can be seen. Respond with text.",
    },
    {
        "role": "user",
        "content": [
            {"type": "image", "image": "photo.jpg"},
            {"type": "audio", "audio": "question.wav"},
        ],
    },
]
inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(device)

with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=1024, generation_mode="text")

print(processor.decode(
    outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
    skip_special_tokens=True,
))

Include the video and a question in the user turn. No specific system prompt is required. Video is sampled at 2 frames per second (4 to 384 frames), and each pair of frames is prefixed with its timestamp.

conversation = [
    {
        "role": "user",
        "content": [
            {"type": "video", "video": "clip.mp4"},
            {"type": "text", "text": "What happens in this video?"},
        ],
    },
]
inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
).to(device)

with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=1024, generation_mode="text")

print(processor.decode(
    outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
    skip_special_tokens=True,
))

To let the model see and hear a video, use the system prompt Answer questions about the video using what you see and hear. and pass two flags:

  • load_audio_from_video=True reads the audio track out of the video file.
  • use_audio_in_video=True interleaves that audio with the frames, so each pair of frames is followed by the sound that goes with it.

Without them, the model gets the frames only.

conversation = [
    {"role": "system", "content": "Answer questions about the video using what you see and hear."},
    {
        "role": "user",
        "content": [
            {"type": "video", "video": "clip.mp4"},
            {"type": "text", "text": "What happens here, and what can you hear?"},
        ],
    },
]
inputs = processor.apply_chat_template(
    conversation,
    add_generation_prompt=True,
    tokenize=True,
    return_dict=True,
    return_tensors="pt",
    load_audio_from_video=True,
    processor_kwargs={"use_audio_in_video": True},
).to(device)

with torch.no_grad():
    outputs = model.generate(**inputs, max_new_tokens=1024, generation_mode="text")

print(processor.decode(
    outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
    skip_special_tokens=True,
))

By default, LFM3-Omni-2B samples video at 2 fps, with at least 4 and at most 384 frames per video. Each frame adds vision tokens, so for long videos a lower frame rate or frame limit uses less context and memory. These settings do not change the soundtrack: the model always hears all of it.

  • Transformers: set the frame rate per call with processor.apply_chat_template(..., processor_kwargs={"fps": 1}). With the soundtrack, use processor_kwargs={"use_audio_in_video": True, "fps": 1}. Set the frame limit when you load the processor: AutoProcessor.from_pretrained(model_id, trust_remote_code=True, max_frames=128).
  • SGLang: change fps, min_frames, and max_frames under video_processor in processor_config.json in a local copy of the checkpoint.
  • llama.cpp: always uses 2 fps and 4 to 384 frames for this model. --video-fps has no effect.

Pass function schemas through tools=... to apply_chat_template(). The chat template places them in the system prompt between <tools> and </tools>. Requests may be spoken or written. A tool call looks like this:

<tool_call> <function=set_volume> <parameter=volume> 30 </parameter> </function> </tool_call>

Values other than strings are written as JSON. Your application parses and runs the calls, then appends the assistant's tool_calls (with arguments as a dict) and a tool message with the results to the conversation. Generate again with the same tools: the model either makes more calls or answers.

import json
import re

tools = [{
    "type": "function",
    "function": {
        "name": "set_volume",
        "description": "Set the device speaker volume.",
        "parameters": {
            "type": "object",
            "properties": {
                "volume": {"type": "integer", "description": "Volume from 0 to 100."}
            },
            "required": ["volume"],
        },
    },
}]
functions = {"set_volume": lambda volume: {"volume": volume}}  # your implementations

CALL_RE = re.compile(r"<tool_call>\s*<function=([^>\n]+)>(.*?)</function>\s*</tool_call>", re.DOTALL)
PARAM_RE = re.compile(r"<parameter=([^>\n]+)>\n?(.*?)\n?</parameter>", re.DOTALL)


def parse_tool_calls(text):
    calls = []
    for name, body in CALL_RE.findall(text):
        arguments = {}
        for key, value in PARAM_RE.findall(body):
            try:
                arguments[key] = json.loads(value)
            except json.JSONDecodeError:
                arguments[key] = value  # plain string
        calls.append({"type": "function", "function": {"name": name, "arguments": arguments}})
    return calls


def respond(conversation):
    inputs = processor.apply_chat_template(
        conversation,
        tools=tools,
        add_generation_prompt=True,
        tokenize=True,
        return_dict=True,
        return_tensors="pt",
    ).to(device)
    with torch.no_grad():
        outputs = model.generate(**inputs, max_new_tokens=1024, generation_mode="text")
    return processor.decode(
        outputs.sequences[0, inputs["input_ids"].shape[1]:outputs.sequence_lengths[0]],
        skip_special_tokens=True,
    )


conversation = [
    {"role": "user", "content": [{"type": "audio", "audio": "question.wav"}]},
]
text = respond(conversation)
while calls := parse_tool_calls(text):
    results = [functions[c["function"]["name"]](**c["function"]["arguments"]) for c in calls]
    conversation += [
        {"role": "assistant", "tool_calls": calls},
        {"role": "tool", "content": json.dumps(results)},
    ]
    text = respond(conversation)

print(text)

See the Tool Use documentation for details.

Fine-tuning

LFM3-Omni-2B is designed for easy customization for a given domain, task, or application with Hugging Face Transformers.

Performance

Omni

Vision

Audio

Citation

@article{liquidAI2026LFM3,
  author  = {Liquid AI},
  title   = {Introducing LFM3: Every Modality for Specialized Intelligence},
  journal = {Liquid AI Blog},
  year    = {2026},
  note    = {www.liquid.ai/blog/lfm3},
}

For questions and feedback, join Discord. For custom deployment and fine-tuning support, contact Liquid AI.

Want more deterministic results?

Interfaze

logo

Product

Playground

OCR

Models

Leaderboards

Pricing

OpenWebSearch

DefaultModel