feat(voice): swap kokoro TTS → piper-tts

Kokoro has been stale upstream since April 2025 (`requires_python<3.13`),
which broke the Python 3.14 build. Piper is the active replacement:
maintained by OHF/Home Assistant, depends only on onnxruntime +
pathvalidate (no torch, no spacy, no transformers), and has cp314
support today.

Dockerfile:
- Add `pip install piper-tts` after the STT install.
- Bundle two default voices (en_US-amy-medium, en_US-ryan-medium) into
  /opt/piper-voices at build. Additional voices can be downloaded into
  /data/voices via the admin UI (separate commit).
- Image add over the STT-only baseline: ~150 MB.

services/tts.py — full rewrite:
- New voice-discovery layer scans /opt/piper-voices + /data/voices for
  .onnx + .onnx.json pairs. /data wins over /opt for the same id so
  admin-downloaded voices can override bundled defaults.
- Single PiperVoice kept warm; switches via _switch_voice() when the
  user changes their voice_tts_voice setting.
- list_voices() returns metadata read from .onnx.json sidecars (label
  derived from filename, language, quality, sample_rate).
- synthesise() uses piper's SynthesisConfig; converts kokoro-shaped
  `speed` multiplier to piper's `length_scale` (1.0 / speed).
- `voice_blend` parameter accepted but ignored — piper has no blend
  equivalent; first entry's voice is used if anything is passed.
- Dropped: HuggingFace commit-hash tracking (~80 lines), the daily
  check_for_kokoro_updates task, voice-tensor blending math.

routes/voice.py:
- tts_backend reports "piper" in /api/voice/status.
- /api/voice/voices no longer requires tts_available() — even with
  the active voice failed to load, the catalog still lets the user
  pick a different one.
- Synthesise request body dropped the voice_blend field; speed and
  voice still supported.

alembic 0047_reset_voice_tts_settings:
- Deletes any stored voice_tts_voice (kokoro IDs that don't map to
  piper) and voice_tts_blend (no piper equivalent) rows. Both
  re-default cleanly on next read.

frontend:
- VoiceBlendEntry type removed from api/client.ts.
- synthesiseSpeech() signature dropped the voiceBlend parameter.
- SettingsView.vue Voice Blend section removed entirely (slider,
  preview, slot management). voice_tts_blend save path removed.
- Default voice id changed from "af_heart" to "en_US-amy-medium".
- VoiceEntry gains optional language/quality/sample_rate fields
  from the richer piper sidecar metadata.

Voice paths remain lazily guarded — `VOICE_ENABLED=false` (default)
starts the app cleanly regardless of which TTS deps are present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 07:59:09 -04:00
parent c91b9c46ff
commit a28f75994a
6 changed files with 317 additions and 363 deletions
+20 -2
View File
@@ -21,11 +21,29 @@ RUN --mount=type=cache,target=/root/.cache/pip \
# Python; the actual inference engine is ctranslate2 (C++) which has
# cp314 wheels as of v4.7.2 (2026-05-19). No torch needed — ctranslate2
# does its own CPU inference. Image add: ~150 MB.
# TTS (piper-tts) is installed in a separate later step so STT can
# stand alone if TTS becomes problematic.
RUN --mount=type=cache,target=/root/.cache/pip \
pip install faster-whisper soundfile
# Text-to-speech (piper-tts). Replaces kokoro, which has been
# stale upstream since April 2025 (requires_python<3.13). Piper depends
# only on onnxruntime (already pulled in for STT via faster-whisper) and
# pathvalidate — total Python overhead is tiny. Voice models are separate
# .onnx + .onnx.json files bundled below.
RUN --mount=type=cache,target=/root/.cache/pip \
pip install piper-tts
# Bundle two default voices in the image so first-run TTS works offline.
# Additional voices can be downloaded at runtime into /data/voices via the
# admin UI (see services/tts.py for the voice-discovery logic).
# Voice catalog: https://huggingface.co/rhasspy/piper-voices
RUN mkdir -p /opt/piper-voices && cd /opt/piper-voices && \
for VOICE in en_US-amy-medium en_US-ryan-medium; do \
DATASET="${VOICE#en_US-}"; DATASET="${DATASET%-medium}"; \
BASE="https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/${DATASET}/medium"; \
curl -fsSL -o "${VOICE}.onnx" "${BASE}/${VOICE}.onnx"; \
curl -fsSL -o "${VOICE}.onnx.json" "${BASE}/${VOICE}.onnx.json"; \
done
# Build the fable-mcp wheel so it can be served for download
COPY fable-mcp/ fable-mcp/
RUN --mount=type=cache,target=/root/.cache/pip \