diff --git a/Dockerfile b/Dockerfile index 84b5971..2341478 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/alembic/versions/0047_reset_voice_tts_settings.py b/alembic/versions/0047_reset_voice_tts_settings.py new file mode 100644 index 0000000..98bff53 --- /dev/null +++ b/alembic/versions/0047_reset_voice_tts_settings.py @@ -0,0 +1,38 @@ +"""reset voice_tts_voice and voice_tts_blend settings for piper migration + +Revision ID: 0047 +Revises: 0046 +Create Date: 2026-05-22 + +The TTS backend swapped from kokoro to piper. Kokoro voice IDs +(`af_heart`, `am_adam`, etc.) don't map to piper voice files +(`en_US-amy-medium`, etc.) and there's no sensible auto-translation. +Clear the stored voice selection so every user falls back to the piper +default the next time they synthesize. + +`voice_tts_blend` goes away entirely — piper has no voice-blending +equivalent. The TTS service accepts the field for backward compat but +ignores it; clearing the rows makes the DB match reality. + +Both settings re-default cleanly on read, so this migration just deletes +the rows without backfilling anything. +""" +from alembic import op + + +revision = "0047" +down_revision = "0046" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute( + "DELETE FROM settings WHERE key IN ('voice_tts_voice', 'voice_tts_blend')" + ) + + +def downgrade() -> None: + # No-op. The deleted settings just re-default on read; restoring the + # kokoro IDs wouldn't help anyway since kokoro is gone. + pass diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 19607c2..52305a0 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -623,11 +623,9 @@ export interface VoiceStatusResult { export interface VoiceEntry { id: string label: string -} - -export interface VoiceBlendEntry { - voice: string - weight: number + language?: string + quality?: string + sample_rate?: number } export const getVoiceStatus = () => apiGet('/api/voice/status') @@ -651,16 +649,15 @@ export async function synthesiseSpeech( text: string, voice?: string, speed?: number, - voiceBlend?: VoiceBlendEntry[] ): Promise { - // Only send voice/speed/blend when explicitly provided — omitting them lets - // the server auto-load the user's saved voice settings (voice, speed, blend). + // Only send voice/speed when explicitly provided — omitting them lets the + // server auto-load the user's saved voice settings. + // Voice blending was removed with the kokoro → piper migration (piper has + // no blend equivalent); callers that previously passed `voiceBlend` should + // pass the first voice's id as `voice` instead. const body: Record = { text } - if (voiceBlend && voiceBlend.length >= 2) { - body.voice_blend = voiceBlend - if (speed !== undefined) body.speed = speed - } else if (voice !== undefined || speed !== undefined) { - body.voice = voice ?? 'af_heart' + if (voice !== undefined || speed !== undefined) { + body.voice = voice ?? 'en_US-amy-medium' body.speed = speed ?? 1.0 } const res = await fetch('/api/voice/synthesise', { diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 21a2f8a..772b8fa 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -1,10 +1,9 @@