From c701ebf6317f98a48cce51efc729b49705f4b0bf Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 16 Apr 2026 19:48:14 +0200 Subject: [PATCH] add some more piper languages --- docker-compose.dev.yml | 2 +- scripts/download-piper-extra-voices.sh | 27 ++++++++++++++++ services/piper-tts-proxy/server.ts | 8 ++--- src/lib/read-aloud.ts | 4 +-- src/lib/trinity-languages.test.ts | 37 +++++++++++++++++++--- src/lib/trinity-languages.ts | 43 ++++++++++++++++++++++---- 6 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 scripts/download-piper-extra-voices.sh diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1863dd49..8df1dd1e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -79,7 +79,7 @@ services: restart: unless-stopped # Wyoming Piper + HTTP bridge (read-aloud). Profile local-tts — matches vite /api/piper-tts → :9876, Wyoming :10200. - # Mount voices under ./.local-piper-data/voices (see PROXY_SETUP.md) or use your existing piper-data path. + # Mount voices under ./.local-piper-data (flat .onnx + .onnx.json). Extra voices: scripts/download-piper-extra-voices.sh piper-wyoming: image: ${WYOMING_PIPER_IMAGE:-silberengel/wyoming-piper:latest} profiles: ['local-tts'] diff --git a/scripts/download-piper-extra-voices.sh b/scripts/download-piper-extra-voices.sh new file mode 100644 index 00000000..52e9db40 --- /dev/null +++ b/scripts/download-piper-extra-voices.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Download Piper ONNX voices from rhasspy/piper-voices into .local-piper-data (mounted as /data for piper-wyoming). +# Usage: bash scripts/download-piper-extra-voices.sh +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DEST="${ROOT}/.local-piper-data" +HF="https://huggingface.co/rhasspy/piper-voices/resolve/main" + +mkdir -p "$DEST" + +fetch_pair() { + local relpath="$1" + local base_name + base_name="$(basename "$relpath")" + echo "Fetching ${base_name} ..." + curl -fsSL -o "${DEST}/${base_name}.onnx" "${HF}/${relpath}.onnx" + curl -fsSL -o "${DEST}/${base_name}.onnx.json" "${HF}/${relpath}.onnx.json" +} + +# Paths match rhasspy/piper-voices `voices.json` and EXTRA_READ_ALOUD_PIPER_VOICE / server getVoiceForLanguage. +fetch_pair "ar/ar_JO/kareem/medium/ar_JO-kareem-medium" +fetch_pair "it/it_IT/paola/medium/it_IT-paola-medium" +fetch_pair "pt/pt_BR/cadu/medium/pt_BR-cadu-medium" + +# Japanese: rhasspy/piper-voices has no ja_* ONNX; the app still uses Chinese Piper as a CJK-related read-aloud voice for `ja`. + +echo "Done. Files are in ${DEST}. Restart piper-wyoming if it is already running." diff --git a/services/piper-tts-proxy/server.ts b/services/piper-tts-proxy/server.ts index 5944ff2c..b0304f44 100644 --- a/services/piper-tts-proxy/server.ts +++ b/services/piper-tts-proxy/server.ts @@ -1024,18 +1024,18 @@ function detectLanguage(text: string): string { * To see available voices, check the piper-data folder or Wyoming server logs. */ function getVoiceForLanguage(lang: string): string { - // Voice map keys / ids: keep in sync with `src/lib/trinity-languages.ts` (`TRINITY_PIPER_VOICE`). + // Voice map keys / ids: keep in sync with `src/lib/trinity-languages.ts` (`TRINITY_PIPER_VOICE`, `EXTRA_READ_ALOUD_PIPER_VOICE`). const voiceMap: Record = { 'en': 'en_US-lessac-medium', // Default English voice 'de': 'de_DE-thorsten-medium', // German 'fr': 'fr_FR-siwis-medium', // French 'es': 'es_ES-davefx-medium', // Spanish - // 'it': 'it_IT-riccardo-medium', // Italian - not available + 'it': 'it_IT-paola-medium', // Italian (rhasspy/piper-voices; install via scripts/download-piper-extra-voices.sh) 'ru': 'ru_RU-ruslan-medium', // Russian 'zh': 'zh_CN-huayan-medium', // Chinese - // 'ar': 'ar_SA-hafez-medium', // Arabic - not available + 'ar': 'ar_JO-kareem-medium', // Arabic (rhasspy/piper-voices; install via scripts/download-piper-extra-voices.sh) 'pl': 'pl_PL-darkman-medium', // Polish - // 'pt': 'pt_BR-edresson-medium', // Portuguese - not available + 'pt': 'pt_BR-cadu-medium', // Portuguese (BR; rhasspy/piper-voices; same script) 'nl': 'nl_NL-mls-medium', // Dutch 'cs': 'cs_CZ-jirka-medium', // Czech 'tr': 'tr_TR-dfki-medium', // Turkish diff --git a/src/lib/read-aloud.ts b/src/lib/read-aloud.ts index f940c7de..e2a8dffe 100644 --- a/src/lib/read-aloud.ts +++ b/src/lib/read-aloud.ts @@ -4,7 +4,7 @@ import { getNoteTranslation } from '@/lib/note-translation-display' import { getPiperVoiceForChosenLanguage, isTrinityLanguageCode, - TRINITY_LANGUAGE_DISPLAY_NAMES + piperReadAloudProfileLabel } from '@/lib/trinity-languages' import { takeReadAloudTranslationForEvent } from '@/lib/read-aloud-translation-override' import { @@ -738,7 +738,7 @@ export async function speakNoteReadAloud(event: Event): Promise const piperVoiceRequestedLanguageName = piperNotice const piperVoiceProfileName = usedEnglishVoiceFallback || usedRelatedVoiceFallback - ? TRINITY_LANGUAGE_DISPLAY_NAMES[piperProfileCode] + ? piperReadAloudProfileLabel(piperProfileCode) : '' if (READ_ALOUD_TTS_URL) { diff --git a/src/lib/trinity-languages.test.ts b/src/lib/trinity-languages.test.ts index 2a02fa04..78f15b3d 100644 --- a/src/lib/trinity-languages.test.ts +++ b/src/lib/trinity-languages.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest' -import { getPiperVoiceForChosenLanguage, TRINITY_PIPER_VOICE } from '@/lib/trinity-languages' +import { + EXTRA_READ_ALOUD_PIPER_VOICE, + getPiperVoiceForChosenLanguage, + TRINITY_PIPER_VOICE +} from '@/lib/trinity-languages' describe('getPiperVoiceForChosenLanguage', () => { it('uses native Piper for trinity codes', () => { @@ -27,10 +31,19 @@ describe('getPiperVoiceForChosenLanguage', () => { expect(r.piperProfileCode).toBe('ru') }) - it('routes Portuguese to Spanish Piper', () => { + it('uses native Portuguese Piper for pt', () => { const r = getPiperVoiceForChosenLanguage('pt') - expect(r.voice).toBe(TRINITY_PIPER_VOICE.es) - expect(r.usedRelatedVoiceFallback).toBe(true) + expect(r.voice).toBe(EXTRA_READ_ALOUD_PIPER_VOICE.pt) + expect(r.usedEnglishVoiceFallback).toBe(false) + expect(r.usedRelatedVoiceFallback).toBe(false) + expect(r.piperProfileCode).toBe('pt') + }) + + it('uses native Italian Piper for it', () => { + const r = getPiperVoiceForChosenLanguage('it') + expect(r.voice).toBe(EXTRA_READ_ALOUD_PIPER_VOICE.it) + expect(r.usedRelatedVoiceFallback).toBe(false) + expect(r.piperProfileCode).toBe('it') }) it('routes Dutch-adjacent tags to German when not trinity native', () => { @@ -39,8 +52,22 @@ describe('getPiperVoiceForChosenLanguage', () => { expect(r.usedRelatedVoiceFallback).toBe(true) }) - it('falls back to English Piper for unmapped languages', () => { + it('uses native Arabic Piper when base is Arabic', () => { const r = getPiperVoiceForChosenLanguage('ar') + expect(r.voice).toBe(EXTRA_READ_ALOUD_PIPER_VOICE.ar) + expect(r.usedEnglishVoiceFallback).toBe(false) + expect(r.usedRelatedVoiceFallback).toBe(false) + expect(r.piperProfileCode).toBe('ar') + }) + + it('uses Arabic Piper for regional Arabic tags', () => { + const r = getPiperVoiceForChosenLanguage('ar-SA') + expect(r.voice).toBe(EXTRA_READ_ALOUD_PIPER_VOICE.ar) + expect(r.piperProfileCode).toBe('ar') + }) + + it('falls back to English Piper for unmapped languages', () => { + const r = getPiperVoiceForChosenLanguage('hi') expect(r.voice).toBe(TRINITY_PIPER_VOICE.en) expect(r.usedEnglishVoiceFallback).toBe(true) expect(r.usedRelatedVoiceFallback).toBe(false) diff --git a/src/lib/trinity-languages.ts b/src/lib/trinity-languages.ts index 65326faa..454dfeaf 100644 --- a/src/lib/trinity-languages.ts +++ b/src/lib/trinity-languages.ts @@ -1,7 +1,7 @@ /** - * Piper voices match `services/piper-tts-proxy/server.ts` `getVoiceForLanguage`. + * Piper voices match `services/piper-tts-proxy/server.ts` `getVoiceForLanguage` (`TRINITY_PIPER_VOICE` + `EXTRA_READ_ALOUD_PIPER_VOICE`). * Read-aloud uses {@link getPiperVoiceForChosenLanguage}: native Piper for trinity UI codes, then - * **related** Piper (e.g. Chinese for Japanese/Korean, Russian for Ukrainian, Spanish for Portuguese), + * **related** Piper (e.g. Chinese for Japanese/Korean — no `ja`/`ko` in rhasspy/piper-voices yet), * then **English** when no heuristic fits. * * **Translate UIs** use `filterTranslateLanguagesWithGrammarCatalog` in `language-display-meta.ts`: @@ -44,6 +44,18 @@ export const TRINITY_PIPER_VOICE: Record = { tr: 'tr_TR-dfki-medium' } +/** + * Read-aloud Piper voices beyond app UI locales (rhasspy/piper-voices). Install files into + * `.local-piper-data` — see `scripts/download-piper-extra-voices.sh`. + */ +export const EXTRA_READ_ALOUD_PIPER_VOICE: Record = { + ar: 'ar_JO-kareem-medium', + it: 'it_IT-paola-medium', + pt: 'pt_BR-cadu-medium' +} + +export type PiperReadAloudProfileCode = TrinityLanguageCode | keyof typeof EXTRA_READ_ALOUD_PIPER_VOICE + export const TRINITY_FALLBACK_ENGLISH_VOICE = TRINITY_PIPER_VOICE.en /** Native autonyms / labels for **app UI** locales (General settings); not the full translate menu. */ @@ -60,6 +72,17 @@ export const TRINITY_LANGUAGE_DISPLAY_NAMES: { [K in TrinityLanguageCode]: strin tr: 'Türkçe' } +export const PIPER_READ_ALOUD_PROFILE_LABELS: Record = { + ...TRINITY_LANGUAGE_DISPLAY_NAMES, + ar: 'العربية', + it: 'Italiano', + pt: 'Português' +} + +export function piperReadAloudProfileLabel(code: PiperReadAloudProfileCode): string { + return PIPER_READ_ALOUD_PROFILE_LABELS[code] +} + export function isTrinityLanguageCode(s: string): s is TrinityLanguageCode { return TRINITY_SET.has(s as TrinityLanguageCode) } @@ -89,8 +112,8 @@ export type PiperVoiceResolution = { usedEnglishVoiceFallback: boolean /** True when using another trinity voice (e.g. `zh`) to approximate the requested language. */ usedRelatedVoiceFallback: boolean - /** Which trinity Piper profile is actually used (native, related, or `en`). */ - piperProfileCode: TrinityLanguageCode + /** Piper profile for UI labels (trinity locale or e.g. `ar` for Arabic read-aloud). */ + piperProfileCode: PiperReadAloudProfileCode } /** @@ -113,10 +136,8 @@ const RELATED_PIPER_FOR_BASE: Record = { sk: 'cs', sl: 'de', hr: 'ru', - pt: 'es', ca: 'es', gl: 'es', - it: 'es', ro: 'es', la: 'es', sq: 'es', @@ -168,6 +189,16 @@ export function getPiperVoiceForChosenLanguage(rawLang: string): PiperVoiceResol return getPiperVoiceForTrinityLanguage(full) } + const extraVoice = EXTRA_READ_ALOUD_PIPER_VOICE[base] + if (extraVoice) { + return { + voice: extraVoice, + usedEnglishVoiceFallback: false, + usedRelatedVoiceFallback: false, + piperProfileCode: base as PiperReadAloudProfileCode + } + } + const related = RELATED_PIPER_FOR_BASE[full] ?? RELATED_PIPER_FOR_BASE[base] ?? null if (related) {