~/satyajit

PersonaPlex: Moshi learns to take a system prompt

mdjsonmcp

2026-10-12 · 26 min · speech · voice-agents · realtime · audio · nvidia · fine-tuning

Why read this

Notabletop 60%

Full-duplex speech frame by frame, a header diff against Moshi, the prompt's 240 s window, and the benchmark columns the card leaves out.

  • Original analysis
  • Runs on a consumer GPU
  • Widely used

Speech & audioMixed licencesPractitioner model

How this was scored
Is it new?
1 of 3: An incremental tweak
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
2 of 3: A reference for a year or more
Does it affect many?
2 of 3: A widely used model, tool or lab release
Only here?
2 of 3: A teardown or measurement few others did

Score 65 of 100, ranked 188 of 494 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

A repost went round on 11 October: NVIDIA "really removed the biggest friction point in Voice AI", open-sourcing PersonaPlex-7B, "a full-duplex conversational model that can listen and speak at the same time", "100% Open-Source and Free". The replies did the fact-checking before I got there. "Brudda this is almost a year old." "They released it 9 months ago."

They are right about the date. The arXiv paper was submitted on 14 January 2026, the first commit in NVIDIA/personaplex ("Adding PersonaPlex inference code.") is dated the same day, and the model card and NVIDIA's project page both give 15 January 2026 as the release. The Hugging Face repo itself was created on 31 December 2025, presumably private until launch. The last commit to the code is from 2 March. So this is a nine-month-old model, and the post is news about the post.

I still wanted to look, because PersonaPlex answers a question I had about Moshi. Kyutai's Moshi (September 2024) was the first open model that really listens while it talks, and it was also stubbornly itself: one voice, one personality, no system prompt. Every voice agent I would want to build needs a role ("you work for this bank, verify the customer, here are the plans") and usually a specific voice. PersonaPlex is NVIDIA's answer, and the interesting part is how you give a system prompt to a model that has no notion of turns.

NVIDIA/personaplex@3428dfd · snapshot 2026-10-12
tracked files
109
license
MIT
branch
main
tests
3 files
source
377.9 kB
commit date
2026-03-02
source by language
Python282.6 kB(26)TypeScript91.4 kB(42)CSS2.0 kB(1)Dockerfile1.0 kB(2)HTML0.5 kB(1)JavaScript0.3 kB(2)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-12 at 3428dfd — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow, testFileCount

shallow clone: counts describe the pinned tree, not the history

What is actually in the checkpoint

The README says it plainly: "PersonaPlex is based on the Moshi architecture and weights." The Hugging Face card lists kyutai/moshiko-pytorch-bf16 as the base model. So before reading any code I diffed the files.

The weights are gated, but a byte-identical mirror (same file names, same LFS sizes to the byte, 16,742,874,000 for model.safetensors) is not, so I read the safetensors header from it with two HTTP range requests and did the same for Kyutai's ungated Moshiko. A few things fell out.

The audio codec and the text tokenizer are Kyutai's, untouched. tokenizer-e351c8d8-checkpoint125.safetensors (Mimi) and tokenizer_spm_32k_3.model have the same LFS hashes in both repos. NVIDIA did not retrain the codec.

The language model is Moshiko plus more depth transformer. Moshiko has 355 tensors and 7,687,729,152 parameters. PersonaPlex has 475 tensors and 8,371,408,896, all BF16. Nothing was removed. 120 tensors are new and 12 changed shape, and every one of them belongs to the depth transformer: its per-step gating, input projections, codebook embeddings and output heads went from 8 steps to 16, and its attention projections doubled to match. The difference is 683,679,744 parameters, which is exactly eight more depth-transformer steps. "7B" is the family name; the file holds 8.37 billion.

The shared weights moved, but not much. I pulled a few tensors by byte range and compared them: the first depth step's output head differs from Moshiko's by about 3.6% in norm, its input projection by 4.2%, a layer-norm scale in the main transformer by 0.4%. The final output norm is bit-identical. A short fine-tune looks like this, and it matches the paper's recipe: 24,576 steps, learning rates of 2e-6 and 4e-6, six hours on eight A100s for the paper's model.

The eight new steps are not copies. The loader has a patch that fills steps 8 to 15 by copying steps 0 to 7 when they are missing (loaders.py, "Patch 2"), which is how a Moshiko-shaped checkpoint becomes a 16-step model at initialisation. But in the released file linears.8 differs from linears.0 by about 7% and depformer_in.8 from depformer_in.0 by 15%, so they were trained. I come back to what those steps do, because at inference they do nothing useful.

Seventeen streams at 12.5 Hz

The fastest way to understand a full-duplex model is to stop thinking about turns. Moshi, and so PersonaPlex, is a language model over a grid. The rows are token streams. The columns are time, one every 80 ms.

The columns come from Mimi. It takes 24 kHz audio, runs a causal convolutional encoder with strides 8, 6, 5 and 4 (960 samples per step, 25 Hz), a small transformer, and one more 2x downsampling to 12.5 Hz. Each 80 ms frame (1,920 samples) is quantised by a residual vector quantiser: the loader builds 32 codebooks of 2,048 entries and then calls set_num_codebooks(8), so each frame is eight 11-bit tokens. Eight times 11 bits at 12.5 frames a second is 1,100 bits per second, the 1.1 kbps the Moshi paper quotes. The first codebook is distilled toward semantic features; the other seven carry the acoustic detail.

The rows are what _lm_kwargs in moshi/moshi/models/loaders.py declares:

# NVIDIA/personaplex @ 3428dfd, moshi/moshi/models/loaders.py
_lm_kwargs = {
    "dim": 4096, "text_card": 32000, "existing_text_padding_id": 3,
    "n_q": 16, "dep_q": 8,           # overridden to 16 in get_moshi_lm()
    "card": 2048, "num_heads": 32, "num_layers": 32,
    "context": 3000,
    "depformer_dim": 1024, "depformer_num_layers": 6,
    "delays": [0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1],
}

Seventeen delays, seventeen streams. Stream 0 is the agent's text. Streams 1 to 8 are the agent's own eight audio codebooks. Streams 9 to 16 are the user's eight codebooks, from the microphone. The delays say that within each speaker the semantic codebook is on time and the seven acoustic codebooks lag it by one frame.

Two transformers split the work. The temporal transformer is the big one: 32 layers, width 4,096, the Helium 7B language model that Moshi was built on. At each frame it sums the embeddings of all 17 tokens from the previous column into one vector and runs one step. So the user's audio is not a separate input path or a separate encoder; it is eight more embeddings added into the same residual stream as the model's own words. The depth transformer is small (6 layers, width 1,024) and runs within a frame: from the temporal transformer's output it predicts the text token, then codebook 1 conditioned on the text, then codebook 2 conditioned on both, and so on down the column. Moshi's paper calls this an RQ-Transformer, and its trick of predicting text first is the Inner Monologue: the model writes a word-aligned transcript of what it is about to say, one step ahead of the audio for it, padded with PAD tokens between words.

A whole session laid out that way, from the prompt to a barge-in, fits on one tape. Drag through it.

17 streams, one column every 80 mslayout and prompt phases: lm.py, loaders.py at 3428dfd
voice prompt
pause
text prompt
pause
conversation
agent text
1 stream, delay 0
agent audio
codebook 1, delay 0
agent audio
codebooks 2-8, delay 1
user audio
codebook 1, delay 0
user audio
codebooks 2-8, delay 1
conversation · t = 320 msagent rows: sampled by the modeluser rows: from the microphone (dashed)leaving the speaker now: the previous frame, silence

A backchannel. Mid-sentence, the text head samples 'yeah' and the audio follows. Nothing interrupted anything: a short agent utterance overlapping the user is just a likely continuation in two-channel phone data.

speaker samplesilence440 Hz sinespeechbackchanneldashed border = provided, not sampled
Stream layout, delays and prompt phases are read from the inference code. The conversation is a scripted illustration of the mechanism, not a recording; the voice prompt and the pauses are drawn shortened.

Everything outside the agent rows is forced. The user rows come from the microphone through Mimi's encoder, and during the prompt the agent rows are forced too. In conversation, the model samples its text token and its eight audio tokens every 80 ms whether or not anyone is talking. The tape's prompt layout and stream delays come straight from the code. The conversation part is a script I wrote to show the mechanism, not a recording.

Where barge-in and backchannels come from

Nothing in the code decides who holds the floor. I looked for it. There is no voice activity detector between the WebSocket and the model, no end-of-turn classifier, no state machine. server.py decodes Opus from the browser, cuts it into 1,920-sample chunks, encodes each with Mimi, calls lm_gen.step() once, decodes the agent's eight codebooks back to audio and sends it. That loop is the whole policy.

So "listening" is the model predicting that its own next frame is silence. Pausing politely while you think is the model predicting silence for a few more frames when your audio stops. A backchannel ("yeah", "oh okay") is a short agent utterance that overlaps yours, which is a perfectly likely continuation in two-channel telephone data, where people do it constantly. Being interrupted is the user's codebooks suddenly carrying speech while the agent's own stream also carries speech; the model has seen that pattern in training and the likely continuation is to trail off. Every one of these behaviours is the same next-token prediction, conditioned on two streams at once.

It is a cleaner design than a cascade's, and it has a cost I keep seeing in the replies to every full-duplex demo: when the model gets it wrong, there is no knob. A cascade like Hugging Face's speech-to-speech pipeline exposes its turn policy as Smart Turn thresholds in milliseconds. Qwen's Realtime model, which I looked at in Qwen-Audio-3.1, keeps a separate decision model whose job is listen, speak, interrupt or resume. In PersonaPlex the turn policy is spread across 8 billion weights, and the only levers are the sampling temperatures (0.8 for audio, 0.7 for text, top-k 250 and 25 by default) and the training data.

The latency is set by the frame

The latency arithmetic follows from the grid. The model cannot act on audio until a whole 80 ms frame has arrived and been encoded. When it decides to speak at frame t, it samples the text token and codebook 1 for t, but the other seven codebooks for t come out at step t+1 because of the acoustic delay, and Mimi cannot decode the frame until all eight exist. LMGen handles this by returning, at each step, the column from max_delay frames ago. So the floor is one frame to hear plus one frame of delay, 160 ms, which is the theoretical figure the Moshi paper gives (200 ms in practice for Moshi).

The benchmark numbers sit right at that floor. The released checkpoint's smooth-turn-taking latency on Full-Duplex-Bench is 0.170 s, and its time to stop when interrupted is 0.240 s, three frames. The paper's experimental model reported 0.070 s for turn taking, which is less than one frame; a number below 80 ms can only mean it often starts speaking at or before the moment the benchmark marks as the end of the user's turn. Neither paper explains it, and I could not check it.

The other half of latency is compute. Every frame has to finish in 80 ms, and at batch size one a 7B forward pass is bound by memory bandwidth, not FLOPs: the GPU has to read essentially all the weights every step. That is 16.74 GB of BF16 twelve and a half times a second, about 209 GB/s of sustained bandwidth before anything else runs. An A100 or a 4090 has several times that. PCIe does not, which is why the README's --cpu-offload flag will load the model on a small GPU, but I would not expect it to keep up with a live conversation.

Eight depth steps that are thrown away

This is where the extra 683,679,744 parameters come back. The Moshi paper trains the depth transformer on both speakers: in its Figure 4 the tokens above a dashed line are the user's, predicted during training and "fed from the user" at inference. Kyutai's public Moshiko checkpoint ships only the 8 steps for its own stream. PersonaPlex ships all 16 and, at inference, runs all 16.

You can see it in LMGen.depformer_step (lm.py): the loop is for cb_index in range(lm_model.dep_q) with dep_q = 16. For steps 8 to 15 it samples a token and then, because the user's real token is marked as provided, feeds the real one forward instead; process_transformer_output writes sampled tokens into the cache only where(~state.provided ...). The predictions of your audio are computed and discarded every frame. Those eight steps are 8.2% of the checkpoint (683.7M of 8,371.4M parameters), so at batch one about 8% of the weight traffic per frame buys nothing. Making the loop stop at AUDIO_TOKENS_PER_STREAM would be a small, safe saving. I have not timed it.

The new part: a system prompt in a world without turns

A text LLM's system prompt is just text before the conversation. A full-duplex model has no "before": every frame has a user stream, an agent text stream and an agent audio stream, and there is no turn marker to put a prompt behind. NVIDIA's answer is the Hybrid System Prompt, and it is more literal than I expected. They write the prompt into the grid as if it had happened.

Diagram of three input channels over time, under a Mimi codec, a temporal transformer and a depth transformer. In the voice prompt segment, the agent audio channel carries speaker samples, the agent text channel carries PAD tokens and the user audio channel carries sine waves. A pause follows with silent agent audio. In the text prompt segment, the agent text channel carries the tokens system, You, are, system while agent audio is silent and user audio is a sine wave. After another pause, generation begins: the user channel carries microphone audio and the model generates agent text such as Hello, PAD, How and agent audio.
The Hybrid System Prompt: a voice sample forced onto the agent's own audio channel, a pause, the role text forced onto the agent's text channel, another pause, then live generation. The user channel holds a 440 Hz sine until the microphone opens (PersonaPlex paper, Figure 1).

LMGen.step_system_prompts in lm.py is four loops, and each is a sequence of ordinary step() calls with some streams forced:

# NVIDIA/personaplex @ 3428dfd, moshi/moshi/models/lm.py (abridged)
def step_system_prompts(self, mimi):
    self._step_voice_prompt(mimi)   # moshi_tokens = Mimi(voice wav), text = PAD, user = SINE
    self._step_audio_silence()      # moshi_tokens = SILENCE, text = PAD, user = SINE   (6 frames)
    self._step_text_prompt()        # moshi_tokens = SILENCE, text = prompt token, user = SINE
    self._step_audio_silence()      # again, 6 frames = int(0.5 * 12.5)

The voice prompt is a few seconds of the target speaker written onto the agent's own audio stream, with its text stream padded. From the model's point of view, it has just said those words in that voice, and a language model continues in the voice it has been speaking in. There is no speaker encoder; the zero-shot voice cloning is entirely this.

The text prompt is the role, one SentencePiece token per frame on the agent's text stream, with the agent's audio held at a fixed silence code. The model has, in effect, silently thought the instructions in its inner monologue. The "custom text delimiters" the paper mentions are <system> tags that wrap_with_system_tags adds around your prompt, and they are not special tokens. I ran the prompts through the same SentencePiece model: <system> becomes the three pieces ▁<, system and >. The minimal prompt "You enjoy having a good conversation." costs 13 tokens with its tags, the assistant prompt 26, the repo's own customer-service test prompt 73, and the README's astronaut prompt 93.

The user stream carries a 440 Hz sine wave through all of it. The code hard-codes the sine's Mimi tokens (SINE_TOKENS = [430, 1268, 381, 1611, 1095, 1495, 56, 472]) next to the silence tokens. The paper says the sine is there "for stable conditioning". My reading is that it gives the model an unmistakable "nobody is talking to you" signal that real silence, which appears mid-conversation, does not.

The order is deliberate. The paper found no difference between voice-first and text-first, and put the voice first so it can be precomputed. The 18 preset voices are exactly that precomputation. voices.tgz holds NATF0 to NATF3, NATM0 to NATM3, VARF0 to VARF4 and VARM0 to VARM4, each a .pt file with two tensors. I read their pickles' opcodes without loading them: embeddings is a BF16 tensor of shape [frames, 1, 1, 4096], the summed input embeddings of each voice-prompt frame, and cache is the [1, 17, 4] token ring. NATF2 is 51 frames, 4.08 s of speech. Across the 18 presets the prompt runs from 43 to 69 frames, 3.44 to 5.52 s. Replaying stored embeddings skips Mimi's encoder, which is the "prefilling" the paper mentions. You can also pass any .wav and it is encoded live, normalised to -24 LUFS first.

One practical note from the same code: load_voice_prompt_embeddings calls torch.load(path) with no map_location, and the presets' cache tensor was saved on cuda:0. On a CPU-only PyTorch, which the README suggests for offline evaluation, I would expect loading a preset .pt to fail; passing a .wav voice prompt avoids it. I did not run this.

The prompt lives in a 240-second window

I would want to know this before shipping a customer-service agent on it. The prompt is not stored anywhere special. It is just the first frames of the KV cache, and the temporal transformer's cache is a ring: context: 3000 in the config, and RingKVCache in modules/transformer.py has capacity context, with an attention mask of delta < self.context. At 12.5 frames a second, 3,000 frames is 240 seconds of attention.

With the NATF2 voice and the repo's 73-token service prompt, the prefill is 51 + 6 + 73 + 6 = 136 frames. The first conversation frame is 136; the first text-prompt frame is 57. When the cache passes 3,000 frames the oldest positions are overwritten, so the voice prompt starts to leave attention 229 seconds into the call, and the role text is entirely gone just short of 240 seconds. After four minutes the model is conditioned only on the conversation so far, which by then usually carries the persona implicitly, but the price list and the customer's account number in the prompt are no longer in view.

For context, the paper trained on sequences of at most 2,048 tokens, which it gives as 163.84 seconds. So the model has never been trained on a conversation long enough to lose its prompt. I have not run a four-minute call to see what happens. It is a direct consequence of the code, and the fix (pin the prompt frames, or re-inject them) is not in the repository.

How it was taught to follow a role

The weights are the interesting part of the release, and the data behind them is described but not released. The paper's model was fine-tuned from Moshi on synthetic conversations only: 1,840 hours of customer-service dialogues (105,410 of them) and 410 hours of question-answering (39,322). Transcripts came from Qwen3-32B and GPT-OSS-120B, generated top-down from a service domain to a scenario to a two-speaker script, each with a role prompt for the agent. The QA dialogues all used one fixed prompt, "You are a wise and friendly teacher. Answer questions or provide advice in a clear and engaging way." Audio came from TTS. One trick I liked: when stitching single-speaker TTS turns together, "inserting negative-duration silence instead simulates barge-in and interruption."

The training objective is Moshi's, with the prompt masked out of the loss, and with non-semantic audio tokens weighted by 0.02 and padded text tokens by 0.3. Those weights matter more than they look. Seven of every eight agent audio tokens are acoustic, and most text tokens are PAD. Without the down-weighting the loss would be dominated by predicting fine acoustic texture and padding, not by what to say.

The released checkpoint is not that model. Appendix A of the paper says it was additionally trained on 7,303 real conversations from the Fisher English corpus, 1,217 hours, back-annotated with prompts by GPT-OSS-120B at three levels of detail ("You enjoy having a good conversation." up to a full persona). All synthetic speech was re-generated with Chatterbox TTS in voices sampled from TortoiseTTS and pitch-shifted with Praat, "for data privacy", instead of cloning voices from VoxCeleb and LibriTTS. The project page frames the result as two data sources doing two jobs: synthetic dialogues teach task adherence, Fisher teaches how people actually talk, and the shared prompt format lets the model combine them.

The model card's "Training Dataset" section lists only Fisher, "Less than 10,000 Hours". With the synthetic sets that adds up to roughly 3,500 hours, consistent with the project page's "under 5,000 hours of directed data".

The data-scale ablation in the paper (Table 5) is worth a second look. Speaker similarity barely moves between 25%, 50% and 100% of the synthetic data (0.54, 0.56, 0.57). Service-Duplex-Bench adherence rises steadily (4.20, 4.24, 4.48). But the Full-Duplex-Bench GPT-4o score is highest at 50% (4.52) and drops to 4.21 with all of it. More customer-service data seems to cost some general QA quality. The paper reports the table without remarking on that.

Checking the numbers

The model card's benchmark table is the paper's Table 6, the released checkpoint, number for number: pause handling TOR 0.358 (synthetic) and 0.431 (Candor), backchannel TOR 0.273, frequency 0.042 and JSD 0.662, smooth turn taking TOR 0.908 at 0.170 s, user interruption TOR 0.950, GPT-4o score 4.290 and 0.240 s, and speaker similarity 0.650. The card then says "PersonaPlex outperforms other open-source and commercial systems on conversational dynamics, response and interruption latency, and task adherence", with three charts.

Grouped bar chart titled Conversational Dynamics, higher is better. Success rates for Smooth Turn Taking: PersonaPlex 90.8, Moshi 94.1, Freeze Omni 33.6, Gemini Live 65.5, Qwen 2.5 Omni N/A. User Interruption: 95.0, 100.0, 86.7, 89.1, N/A. Pause Handling: 60.6, 1.8, 43.9, 71.8, N/A. Average: 82.1, 65.3, 54.7, 75.5, N/A.
The card's conversational-dynamics chart. Pause handling is plotted as one minus the mean of the two pause TORs, and the average is over these three bars; the backchannel results are not plotted (PersonaPlex model card, results_conversation_dynamics.png).

I checked the chart against the tables. Pause handling is one minus the mean of the two pause TORs: for PersonaPlex 1 − (0.358 + 0.431)/2 = 60.6%, for Gemini 71.8%, for Moshi 1.8%. All of those reproduce. The average of three bars is 82.1 for PersonaPlex and 75.5 for Gemini, and that average is where the "outperforms" comes from.

Read bar by bar, it is less tidy. Moshi, the base model, beats PersonaPlex on smooth turn taking (94.1 against 90.8) and on interruption (100.0 against 95.0). Gemini beats it on pause handling (71.8 against 60.6). And the backchannel column is not in the chart at all, which is the one where Gemini's takeover rate is 0.091 against PersonaPlex's 0.273: Gemini wrongly grabs the turn on a user's "mm-hm" a third as often. The Gemini in these charts is gemini-2.0-flash-live-001, run in September 2025, so it is not the current model either.

Grouped bar chart titled Latency, lower is better, in seconds. Smooth Turn Taking: PersonaPlex 0.170, Moshi 0.265, Freeze Omni 0.953, Gemini Live 1.301, Qwen 2.5 Omni N/A. User Interruption: 0.240, 0.257, 1.409, 1.183, N/A. Average: 0.205, 0.261, 1.181, 1.242, N/A.
Latency is the clearest win, and the one the architecture predicts: a full-duplex model at 12.5 Hz answers in two to three frames, a VAD-gated system in over a second (PersonaPlex model card, results_latency.png).

Latency is where the claim holds outright. 0.170 s and 0.240 s against Gemini Live's 1.301 s and 1.183 s, and Freeze-Omni's 0.953 s and 1.409 s. The credit there goes mostly to Moshi's architecture. Moshi was already at 0.265 s and 0.257 s; PersonaPlex keeps the property while adding the prompt. One discrepancy: the chart shows Qwen 2.5 Omni's interruption latency as N/A, while Table 2 of the paper gives it as 2.740 s.

Grouped bar chart titled Task Adherence, GPT-4o LLM judge score, higher is better. FullDuplexBench: PersonaPlex 4.29, Moshi 0.77, Freeze Omni 3.62, Gemini Live 3.38, Qwen 2.5 Omni 4.59. ServiceDuplexBench: 4.40, 1.75, 4.02, 4.73, 2.76. Average: 4.34, 1.26, 3.82, 4.05, 3.68.
Task adherence, judged by GPT-4o. PersonaPlex leads only on the average of the two benchmarks: Qwen 2.5 Omni is ahead on Full-Duplex-Bench and Gemini Live on NVIDIA's own Service-Duplex-Bench (PersonaPlex model card, results_task_adherence.png).

Task adherence is the same pattern. On Full-Duplex-Bench's QA, Qwen 2.5 Omni scores 4.59 to PersonaPlex's 4.29. On Service-Duplex-Bench, which NVIDIA built for this paper (50 service scenarios, 7 probe questions each), Gemini Live scores 4.73 to PersonaPlex's 4.40; the paper's own text concedes PersonaPlex "outperforms all models except Gemini Live" there. PersonaPlex wins only the average (4.34 against Gemini's 4.05), because Gemini scores 3.38 on the QA set and Qwen 2.76 on the service set. Service-Duplex-Bench was "to be released soon" in January; I searched the Hub for it and found nothing.

The charts are drawn from a larger table. Here it is in full, with the paper's experimental model and the released one side by side.

Full-Duplex-Bench, every column the paper reportspaper Tables 2 and 6 (self-run)
model
Pause
synthetic TOR ↓
Pause
Candor TOR ↓
Backchannel
TOR ↓
not charted
Backchannel
freq ↑
not charted
Backchannel
JSD ↓
not charted
Turn taking
TOR ↑
Turn taking
latency s ↓
Interruption
TOR ↑
Interruption
GPT-4o ↑
Interruption
latency s ↓
PersonaPlex (released, Table 6)0.3580.4310.2730.0420.6620.9080.1700.9504.2900.240
Qwen-2.5-Omni––––––––4.5902.740
Freeze-Omni0.6420.4810.6360.0010.9970.3360.9530.8673.6151.409
Gemini Live0.2550.3100.0910.0120.8960.6551.3010.8913.3761.183
Moshi0.9850.9801.0000.0010.9570.9410.2651.0000.7650.257
dGSLM0.9340.9350.6910.0150.9340.9750.3520.9170.2012.531

PersonaPlex (released) is best, or tied for best, in 4 of 10 columns shown. Green marks the best value in each column.

TOR is takeover rate: how often the model takes the turn. Lower is right for pauses and backchannels, higher for turn ends and interruptions. A model that always grabs the floor scores 1.000 on the interruption TOR and can still answer badly, which is why Moshi’s 1.000 sits next to a GPT-4o score of 0.765.

Counting columns: the released checkpoint is best in four of the ten (backchannel frequency and JSD, and both latencies). Gemini takes three, Moshi, dGSLM and Qwen one each. The experimental model from the paper's main table is best or tied in six. The release traded some of the paper model's eagerness (turn-taking TOR 0.992 down to 0.908, interruption TOR 1.000 down to 0.950) for far better pause handling (0.584 down to 0.358) and backchannel rate, which is what you would expect from adding a thousand hours of real telephone conversation. I think that was the right trade. A model that never lets you finish a thought is worse than one that is a beat slow.

None of this means PersonaPlex is bad. On the thing it set out to do, it is clearly a step: Moshi scores 1.75 on the service benchmark and 0.77 on QA, PersonaPlex 4.40 and 4.29, and speaker similarity to the voice prompt is 0.65 for the release against 0.10 for Moshi. Human naturalness ratings (DMOS) put it first among the five systems in both of the paper's studies, 3.90 in the main one and 2.95 in the release's separate study, with overlapping confidence intervals against Gemini in the second. "Outperforms everything" is just a stronger sentence than the tables support.

"100% open-source and free"

Taking the claim piece by piece:

The code is MIT, and it is inference only: a server, an offline WAV-to-WAV script and a React client. LMModel.forward_train exists, inherited from Moshi, but there is no trainer, no data pipeline and no fine-tuning script.

The weights are under the NVIDIA Open Model License, with an extra notice that the Moshiko base is CC-BY-4.0. The licence is genuinely permissive on the things most people care about: "Models are commercially usable", derivatives are allowed, and NVIDIA claims no ownership of outputs. It is not an OSI licence. Your rights terminate automatically if you sue alleging the model infringes, or if you "bypass, disable, reduce the efficacy of, or circumvent any technical limitation, safety guardrail" without a substitute, and NVIDIA may update the terms. The Hugging Face repo is gated behind accepting it, so "free" also means a login and a token.

The data is not released. Fisher is an LDC corpus you license. The synthetic dialogues, the prompts generated for Fisher, and the evaluation set are not on the Hub.

So: free to use, commercially, under a custom licence, with no way to reproduce the training. A normal open-weights release, in other words. It is not 100% open source, and it was not new this week.

The weights have had a life of their own since. The Hub lists MLX and GGUF quantisations from late January onward, and in June Kyutai, Moshi's authors, released personaplex-rl-seamless, PersonaPlex post-trained with GRPO on the Seamless Interaction corpus to fix exactly the interactivity axes above, under CC BY-NC 4.0 on top of NVIDIA's licence.

nvidia/personaplex-7b-v1@fdaf409 · snapshot 2026-10-12
parameters
8.37B
repo size
17.14 GB
architecture
personaplex
task
audio-to-audio
library
moshi
license
other
safetensors
2 shards
largest file
16.74 GB
files
16
downloads
166.4K
likes
2.8K
gated
auto
languages
en
parameters by dtype
BF168.37B
speech-to-speechagent

repo last modified 2026-03-02

Running it

The README's path is short: install libopus-dev, pip install moshi/. from the repo (the package, moshi-personaplex, is a fork of Kyutai's moshi and keeps its import name), accept the licence on Hugging Face, export HF_TOKEN, and run the server:

SSL_DIR=$(mktemp -d); python -m moshi.server --ssl "$SSL_DIR"
# then open https://localhost:8998, pick a voice (NATF0..VARM4) and type a role prompt

or stream a file through it offline, which is how the benchmarks are run:

python -m moshi.offline --voice-prompt "NATM1.pt" \
  --text-prompt "$(cat assets/test/prompt_service.txt)" \
  --input-wav assets/test/input_service.wav --seed 42424242 \
  --output-wav output.wav --output-text output.json

The card lists the A100 80 GB as the test hardware and only Ampere and Hopper as supported. My estimate for what it needs: 16.74 GB of weights, two Mimi instances at about 0.38 GB each in FP32 (the server keeps a second one, other_mimi, in lockstep), and a temporal KV cache of 32 layers × 2 × 3,000 frames × 4,096 × 2 bytes, about 1.57 GB. Roughly 19 GB before activations and CUDA graphs, so a 24 GB card should hold it, with little to spare. I have not run it. The server takes an asyncio.Lock around every session, so one process serves one conversation at a time.

What I would use it for: a voice front end for a narrow, scripted job, where the whole role fits in a prompt of a hundred tokens, calls are short, and naturalness matters more than reasoning. Booking, intake, a support line's first minute. What I would not use it for: anything needing tools (it has none; the paper lists that as future work), anything needing the facts in the prompt to survive past four minutes, or anything where a wrong interruption costs more than an awkward one. For those a cascade with an explicit turn detector, like speech-to-speech, or a staged thinker-talker server like the one I read in vLLM-Omni, gives you the knobs that a single next-token distribution does not.

The thing PersonaPlex gets right is the idea that a full-duplex model's system prompt has to be something that happened on its streams. Forcing the persona onto the model's own inner monologue and a voice sample onto its own mouth, with a sine tone telling it nobody else is there yet, needs no architectural change at all. The paper says as much: "conditioning can be integrated into duplex speech systems without altering their underlying architecture". I expect every open duplex model after it to do something like this.

How I checked

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "PersonaPlex: Moshi learns to take a system prompt", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026personaplexfullduplex,
  author = {Satyajit Ghana},
  title  = {PersonaPlex: Moshi learns to take a system prompt},
  url    = {https://ai.thesatyajit.com/articles/personaplex-full-duplex},
  year   = {2026}
}
share