Views
No views yet
| approach | what breaks |
|---|---|
| Pause generation, call the API, resume | The model is full-duplex — it is always generating. Pausing produces dead air and desynchronises its sense of time. |
| Call the API, speak the result with TTS | Voice and prosody change mid-conversation. It stops sounding like one speaker. |
user audio ──(transcription)──┐
├──> running transcript ──> LLM + tool schemas
model's own \x02 text ────────┘ │
HTTP call (allowlisted)
│
┌── inner monologue <── one forced token per 80 ms frame <─┘
│
the model speaks the answer itself — same voice, same prosodyforked_server.py's generation loop:1if pending_text_tokens: # a tool result is waiting
2 forced_text = pending_text_tokens.popleft() # override the model's own sample
3elif _nudge["force"] > 0: # else a turn-taking nudge
4 forced_text = _NUDGE_TOKEN
5else:
6 forced_text = None # else the model samples freely
7
8tokens = lm_gen.step(codes[:, :, c:c+1], text_token=forced_text)filler phrase ("let me check that") can be injected the same way the moment a
call starts, so the pause while the API responds sounds deliberate.X-Functions
header on the WebSocket upgrade — so different callers get different tools without
redeploying:1{
2 "prompt": "Decide whether to call a function based on the live conversation.",
3 "filler": "Sure, let me check that for you, one moment.",
4 "allowed_hosts": ["api.open-meteo.com"], // SSRF allowlist — required
5 "functions": [{
6 "name": "get_weather",
7 "description": "Current weather for a location given latitude and longitude.",
8 "endpoint": "https://api.open-meteo.com/v1/forecast",
9 "method": "GET",
10 "param_location": "query",
11 "static_params": { "current_weather": true },
12 "parameters": { // OpenAI tool schema
13 "type": "object",
14 "properties": {
15 "latitude": { "type": "number" },
16 "longitude": { "type": "number" }
17 },
18 "required": ["latitude", "longitude"]
19 }
20 }]
21}1python client/personaplex_client.py \
2 --url wss://<your-host>/api/chat \
3 --api-key "$API_KEY" \
4 --script my_persona.txt \
5 --functions examples/weather.json \
6 --input mic| path | what |
|---|---|
src/reasoner.py | transcript accumulation, LLM tool decision, result → injection |
src/http_executor.py | declarative function spec → HTTP call; SSRF allowlist |
src/session_config.py | per-session X-Functions config, validated |
src/forked_server.py | PersonaPlex server + the injection bridge (see licence below) |
client/personaplex_client.py | mic client; stereo recording; per-turn latency |
bench/ | turn-taking latency harness — see below |
examples/weather.json | a working end-to-end example |
bench/voicebench.py measures the metric that matters for a voice agent:latency = model_speech_start − caller_speech_end1python bench/make_turns.py # synthesise caller turns (macOS `say`)
2python bench/voicebench.py --url "$WS_URL" --runs 5nvidia/personaplex-7b-v1 requires accepting the NVIDIA Open
Model Licence. Weights are not included here.LLM_API_KEY). This is not self-contained speech-to-speech — the
tool-decision loop is text and external.asyncio.Lock around
one LMGen. Multi-worker concurrency on a shared GPU was tested and collapsed —
scale by process-per-GPU or by instance, not by workers per GPU.allowed_hosts
is enforced and private/loopback/link-local IPs are blocked — keep it set.S2S_API_KEY adds
Authorization: Api-Key <key> checked before the WebSocket upgrade. Unset means
the endpoint is open; the server logs a warning at startup.src/reasoner.py, src/http_executor.py, src/session_config.py,
client/, bench/ — original work, © 2026 Abhinav Kalvacherla,
Apache-2.0 (see LICENSE). Each file carries an SPDX header.src/forked_server.py — a fork of PersonaPlex's moshi.server, © NVIDIA
CORPORATION and © Kyutai, MIT. The original notice is retained in the file.
Modifications are listed in NOTICE.