Speech-to-Speech Models

Soniqo supports two native MLX model families for conversational speech-to-speech: PersonaPlex 7B and VoiceChat 11B. Both consume speech and generate model audio directly, but they use different duplex architectures and runtime contracts.

Choose a model

ModelConversation modelBest fit
PersonaPlex 7BSimultaneous full duplex with Moshi, Depformer, and MimiOverlapping listen/speak behavior and 18 selectable voices
VoiceChat 11BContinuous, frame-synchronous duplex with FastConformer, Nemotron-H, EAR-TTS, and a neural codec80 ms streaming frames, text/function events, and checkpoint-native speech

These are conversational speech-to-speech models. Hibiki also maps speech directly to speech, but its task is streaming speech translation, so it is documented separately.

PersonaPlex 7B

Full-duplex speech-to-speech dialogue model based on the Moshi architecture (Kyutai). PersonaPlex 7B generates spoken responses directly from spoken input — no intermediate text pipeline required. The model ships with 18 voice presets and is available in 8-bit (recommended) and 4-bit quantization. 8-bit is the default — it is 30% faster and produces coherent responses, while 4-bit degrades output quality.

VoiceChat 11B

Duplex speech-to-speech reference: SALM-Duplex: Efficient and Direct Duplex Modeling for Speech-to-Speech Language Model (Interspeech 2025), cited by NVIDIA's upstream VoiceChat model card.

M5 Pro, 48 GB, Release, 80 ms live cadence, protected-head INT5: three controlled runs measured whole-pipeline RTF 0.94, 0.92, and 0.92, with about 8.70 GB peak RSS. First spoken text arrived in 44.3–46.7 ms and first playable audio in 74.0–76.4 ms. Those are hot compute figures, not learned turn-taking latency; optimized INT8 performance has not yet been remeasured.

VoiceChat is the other native MLX speech-to-speech runtime in speech-swift. It combines causal FastConformer perception, Nemotron-H duplex text/function channels, standalone text-conditioned EAR-TTS, and a 22.05 kHz neural codec. A session accepts 16 kHz mono audio continuously and returns text events plus one 1,764-sample output frame every 80 ms.

Use VoiceChatModel.load(from:), start a VoiceChatSession, and feed audio with pushAudio. A complete bundle must contain encoder/, llm/, and tts/; older understanding-only bundles are rejected.

Current performance

INT5 sustains aggregate throughput below RTF 1 on the tested M5 Pro. All three runs also held total/frame p95 below 80 ms, at 76.2–78.6 ms; deadline headroom therefore remains narrow. Treat model turn onset and hardware compute latency as separate measurements.

PersonaPlex architecture and usage

PersonaPlex is a multi-stream autoregressive model with three core components:

ComponentDetails
Temporal Transformer32 layers, dim=4096, 32 heads, SwiGLU (hidden_scale=4.125), RoPE, 8-bit quantized (default)
Depformer6 layers, dim=1024, 16 heads, MultiLinear (weights_per_step=true), dep_q=16
Mimi Codec16 codebooks, 12.5 Hz frame rate, 24 kHz audio output

The model processes 17 streams simultaneously: 1 text stream + 8 user audio streams + 8 agent audio streams. This architecture enables full-duplex conversation where the model can listen and speak at the same time.

Voice Presets

PersonaPlex includes 18 built-in voice presets across natural and varied styles:

CategoryPresets
Natural FemaleNATF0, NATF1, NATF2, NATF3
Natural MaleNATM0, NATM1, NATM2, NATM3
Varied FemaleVARF0, VARF1, VARF2, VARF3, VARF4
Varied MaleVARM0, VARM1, VARM2, VARM3, VARM4

Inner Monologue

PersonaPlex generates two parallel streams at every step: 8 audio codebook tokens for the Mimi codec and one text token for the model’s internal monologue. The text stream is what the model is “thinking” as it speaks — it can diverge slightly from the final audio, but in practice it mirrors the spoken response closely enough to use as a live transcript.

The text tokens come back as raw SentencePiece piece IDs. Decode them with the SentencePieceDecoder that PersonaPlex ships:

import PersonaPlex
import AudioCommon

let model = try await PersonaPlexModel.fromPretrained()
let decoder = model.tokenizer  // SentencePieceDecoder?

let result = model.respond(userAudio: userSamples, voice: .NATM0)
let transcript = decoder?.decode(result.textTokens) ?? ""
print(transcript)        // "Sure, I can help with that..."
playAudio(result.audio)  // 24 kHz mono Float32

In streaming mode, respondStream emits textTokens chunks as they are produced — decode them incrementally to drive a live caption view while the audio is still generating. The --transcript CLI flag does exactly this behind the scenes.

Why it matters: SentencePieceDecoder is built on the shared AudioCommon.SentencePieceModel protobuf reader, so PersonaPlex, OmnilingualASR and any future SentencePiece-based model decode through the same tokenizer implementation. See the SentencePieceModel reference.

System Prompts

Pass any custom system prompt as a plain string — no external tokenization needed:

let response = model.respond(
    userAudio: audio,
    voice: .NATM0,
    systemPrompt: "You enjoy having a good conversation."
)

Or use a built-in preset:

CLI Usage

Generate a spoken response from an audio input:

# Basic speech-to-speech
.build/release/speech respond --input question.wav

# Choose a voice preset
.build/release/speech respond --input question.wav --voice NATM0

# Stream audio output during generation
.build/release/speech respond --input question.wav --stream

# Custom system prompt text
.build/release/speech respond --input question.wav --system-prompt-text "You enjoy having a good conversation."

# Use a preset system prompt
.build/release/speech respond --input question.wav --system-prompt customer-service

# Get transcript alongside audio
.build/release/speech respond --input question.wav --transcript

# JSON output with metadata
.build/release/speech respond --input question.wav --json

Options

OptionDescription
--inputInput audio file (WAV, required)
--voiceVoice preset name (e.g., NATM0, VARF2)
--system-promptSystem prompt preset: assistant, focused, customer-service, teacher
--system-prompt-textCustom system prompt text (overrides --system-prompt)
--max-stepsMaximum generation steps
--streamEmit audio chunks during generation
--compileUse MLX compiled inference for faster generation
--transcriptOutput the text transcript alongside audio
--jsonJSON output with metadata

Sampling parameters can also be overridden:

OptionDefaultDescription
--audio-temp0.8Audio token sampling temperature
--audio-top-k250Audio token top-k sampling
--text-temp0.7Text token sampling temperature
--text-top-k25Text token top-k sampling

Streaming

The --stream flag enables real-time audio output. Audio chunks are emitted as they are generated, so playback can begin before the full response is complete. This is particularly useful for interactive applications where low latency matters.

Performance

MetricValue
Real-time factor (RTF)~1.4 (8-bit, near real-time)
Step latency~112 ms/step on M2 Max (8-bit)
Model size (8-bit)~9.1 GB
Peak RAM (8-bit)~11 GB
Model size (4-bit)~4.9 GB
Peak RAM (4-bit)~7 GB
Important

PersonaPlex 7B (8-bit) requires at least 24 GB of RAM. The 4-bit variant fits on 16 GB devices but produces degraded output. On 8 GB devices, neither variant will fit. Use --compile for best performance on supported hardware.

Model Variants

ModelSizeHuggingFace
PersonaPlex-7B (8-bit) recommended9.1 GBaufklarer/PersonaPlex-7B-MLX-8bit
PersonaPlex-7B (4-bit)4.9 GBaufklarer/PersonaPlex-7B-MLX-4bit

Swift API

import PersonaPlex
import AudioCommon

let model = try await PersonaPlexModel.fromPretrained()
let response = model.respond(
    userAudio: userSamples,
    voice: .NATM0,
    systemPrompt: "You are a helpful assistant."
)
try WAVWriter.write(samples: response.audio, sampleRate: 24000, to: URL(filePath: "answer.wav"))