Skip to content

Add VuiTTSService: local streaming TTS with Vui Nano (CUDA / Apple Silicon) - #5709

Open
mogwai wants to merge 4 commits into
pipecat-ai:mainfrom
mogwai:vui-tts
Open

Add VuiTTSService: local streaming TTS with Vui Nano (CUDA / Apple Silicon)#5709
mogwai wants to merge 4 commits into
pipecat-ai:mainfrom
mogwai:vui-tts

Conversation

@mogwai

@mogwai mogwai commented Sep 12, 2026

Copy link
Copy Markdown

What

VuiTTSService — local, streaming text-to-speech with Vui Nano, a small, context-aware model trained on real conversations (219M active / 305M total parameters, Apache 2.0). It runs in-process through the vui-tts package: on an NVIDIA GPU, or on MLX on Apple Silicon. No API key; weights and the shipped voice prompts download from Hugging Face on first use. Audio streams into the pipeline frame by frame as it is decoded.

tts = VuiTTSService(settings=VuiTTSService.Settings(voice="maeve"))

Modelled on PocketTTSService (local model, _stream_audio_frames_from_iterator, settings delta for the voice). New extra pipecat-ai[vui] — marked python_version == '3.12', the only version vui-tts ships for, so other splits lock unchanged.

Design

  • Voice: a shipped name (maeve / abraham / rhian / harry), a prompt .safetensors baked with Vui's scripts/build_prompts.py, or a .wav to clone (sibling .txt transcript, else transcribed). Runtime change via TTSUpdateSettingsFrame re-prefills on the next utterance.
  • Everything that touches the engine — load, prefill, decode, rewind — runs on a single worker thread (CUDA graphs and MLX streams are thread-bound). Interruption sets a cancel event checked at every decoded frame.
  • Metrics: TTFB + usage.

Verified

  • tests/test_vui_tts.py (mocked engine: frame ordering, int16 conversion, one prefill per voice, rewind per turn, voice update) — 2 passed.
  • Real render on an M4 (MLX): a TTSSpeakFrame through the service → 6.6 s of audio in 82 TTSAudioRawFrames, 0.0% WER by moonshine ASR.
  • ruff check / ruff format clean; uv lock regenerated (adds vui-tts 1.1.2 for the 3.12 split only).

Examples: examples/voice/voice-vui.py, examples/update-settings/tts/tts-vui.py. I'm the author of the model (Fluxions AI) and will maintain the service.

Update — CUDA verified too (RTX 5090, torch 2.11, plain pip install without flash-attn): the mocked tests pass and a TTSSpeakFrame through the service renders 5.9 s of audio in 74 TTSAudioRawFrames at 0.0% WER by moonshine. Now requires vui-tts>=1.1.3, which fixes a CUDA multi-turn codec-context issue found in review of the LiveKit sibling plugin.

Scenario verification (exact PR head, MLX on M4 and CUDA on a 5090): four utterances on one row with a TTSUpdateSettingsFrame voice switch mid-way (abraham → maeve), then a real InterruptionFrame mid-utterance followed by a clean utterance — all frames delivered, no ErrorFrames, every utterance 0.0% WER by moonshine on CUDA (MLX: single-word sampling slips only). A 5-rep interrupted-vs-clean comparison showed no interruption-specific degradation; it did show the engine default temperature 0.9 slips more than 0.7, so the service now defaults to 0.7 (matching the LiveKit plugin and the published evals).

…licon)

Vui Nano is a small, context-aware text-to-speech model trained on real
conversations (219M active / 305M total parameters, Apache 2.0). The service
runs the model in-process through the `vui-tts` package — on an NVIDIA GPU,
or on MLX on Apple Silicon — and streams 24 kHz audio into the pipeline as
each frame is decoded. Weights and the shipped voice prompts download from
Hugging Face on first use.

- `pipecat-ai[vui]` extra (Python 3.12 — the only version vui-tts ships for).
- Voice is a shipped name (maeve / abraham / rhian / harry), a prompt
  .safetensors baked with Vui's scripts/build_prompts.py, or a .wav to clone;
  changeable at runtime via TTSUpdateSettingsFrame.
- All engine work (load, prefill, decode, rewind) runs on one worker thread;
  interruption sets a cancel event the decode loop checks at every frame.
- Tests mock the engine; examples for voice and update-settings; docs mock
  list updated.

Verified on an M4 (MLX): a TTSSpeakFrame through the service renders 6.6s of
audio in 82 TTSAudioRawFrames, 0.0% WER by moonshine ASR.
….1.3

vui-tts 1.1.3 makes CUDA Row.rewind() re-seed the codec context from the
voice prompt, so a turn no longer decodes against the previous turn's tail.
A .wav voice without a transcript now raises a clear error (sibling .txt, a
baked prompt .safetensors, or `vui-tts[server]` for transcription) instead
of failing on the missing openai-whisper import. Lock regenerated.
Over repeated renders the engine default of 0.9 drops or swaps the odd word
(5-rep check on MLX and CUDA); 0.7 — what the LiveKit plugin and the
published evals use — is clean. gen_config= still overrides.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant