Browse Source

add some more piper languages

imwald
Silberengel 2 weeks ago
parent
commit
c701ebf631
  1. 2
      docker-compose.dev.yml
  2. 27
      scripts/download-piper-extra-voices.sh
  3. 8
      services/piper-tts-proxy/server.ts
  4. 4
      src/lib/read-aloud.ts
  5. 37
      src/lib/trinity-languages.test.ts
  6. 43
      src/lib/trinity-languages.ts

2
docker-compose.dev.yml

@ -79,7 +79,7 @@ services:
restart: unless-stopped restart: unless-stopped
# Wyoming Piper + HTTP bridge (read-aloud). Profile local-tts — matches vite /api/piper-tts → :9876, Wyoming :10200. # 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: piper-wyoming:
image: ${WYOMING_PIPER_IMAGE:-silberengel/wyoming-piper:latest} image: ${WYOMING_PIPER_IMAGE:-silberengel/wyoming-piper:latest}
profiles: ['local-tts'] profiles: ['local-tts']

27
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."

8
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. * To see available voices, check the piper-data folder or Wyoming server logs.
*/ */
function getVoiceForLanguage(lang: string): string { 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<string, string> = { const voiceMap: Record<string, string> = {
'en': 'en_US-lessac-medium', // Default English voice 'en': 'en_US-lessac-medium', // Default English voice
'de': 'de_DE-thorsten-medium', // German 'de': 'de_DE-thorsten-medium', // German
'fr': 'fr_FR-siwis-medium', // French 'fr': 'fr_FR-siwis-medium', // French
'es': 'es_ES-davefx-medium', // Spanish '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 'ru': 'ru_RU-ruslan-medium', // Russian
'zh': 'zh_CN-huayan-medium', // Chinese '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 '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 'nl': 'nl_NL-mls-medium', // Dutch
'cs': 'cs_CZ-jirka-medium', // Czech 'cs': 'cs_CZ-jirka-medium', // Czech
'tr': 'tr_TR-dfki-medium', // Turkish 'tr': 'tr_TR-dfki-medium', // Turkish

4
src/lib/read-aloud.ts

@ -4,7 +4,7 @@ import { getNoteTranslation } from '@/lib/note-translation-display'
import { import {
getPiperVoiceForChosenLanguage, getPiperVoiceForChosenLanguage,
isTrinityLanguageCode, isTrinityLanguageCode,
TRINITY_LANGUAGE_DISPLAY_NAMES piperReadAloudProfileLabel
} from '@/lib/trinity-languages' } from '@/lib/trinity-languages'
import { takeReadAloudTranslationForEvent } from '@/lib/read-aloud-translation-override' import { takeReadAloudTranslationForEvent } from '@/lib/read-aloud-translation-override'
import { import {
@ -738,7 +738,7 @@ export async function speakNoteReadAloud(event: Event): Promise<ReadAloudResult>
const piperVoiceRequestedLanguageName = piperNotice const piperVoiceRequestedLanguageName = piperNotice
const piperVoiceProfileName = const piperVoiceProfileName =
usedEnglishVoiceFallback || usedRelatedVoiceFallback usedEnglishVoiceFallback || usedRelatedVoiceFallback
? TRINITY_LANGUAGE_DISPLAY_NAMES[piperProfileCode] ? piperReadAloudProfileLabel(piperProfileCode)
: '' : ''
if (READ_ALOUD_TTS_URL) { if (READ_ALOUD_TTS_URL) {

37
src/lib/trinity-languages.test.ts

@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest' 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', () => { describe('getPiperVoiceForChosenLanguage', () => {
it('uses native Piper for trinity codes', () => { it('uses native Piper for trinity codes', () => {
@ -27,10 +31,19 @@ describe('getPiperVoiceForChosenLanguage', () => {
expect(r.piperProfileCode).toBe('ru') expect(r.piperProfileCode).toBe('ru')
}) })
it('routes Portuguese to Spanish Piper', () => { it('uses native Portuguese Piper for pt', () => {
const r = getPiperVoiceForChosenLanguage('pt') const r = getPiperVoiceForChosenLanguage('pt')
expect(r.voice).toBe(TRINITY_PIPER_VOICE.es) expect(r.voice).toBe(EXTRA_READ_ALOUD_PIPER_VOICE.pt)
expect(r.usedRelatedVoiceFallback).toBe(true) 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', () => { it('routes Dutch-adjacent tags to German when not trinity native', () => {
@ -39,8 +52,22 @@ describe('getPiperVoiceForChosenLanguage', () => {
expect(r.usedRelatedVoiceFallback).toBe(true) 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') 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.voice).toBe(TRINITY_PIPER_VOICE.en)
expect(r.usedEnglishVoiceFallback).toBe(true) expect(r.usedEnglishVoiceFallback).toBe(true)
expect(r.usedRelatedVoiceFallback).toBe(false) expect(r.usedRelatedVoiceFallback).toBe(false)

43
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 * 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. * then **English** when no heuristic fits.
* *
* **Translate UIs** use `filterTranslateLanguagesWithGrammarCatalog` in `language-display-meta.ts`: * **Translate UIs** use `filterTranslateLanguagesWithGrammarCatalog` in `language-display-meta.ts`:
@ -44,6 +44,18 @@ export const TRINITY_PIPER_VOICE: Record<TrinityLanguageCode, string> = {
tr: 'tr_TR-dfki-medium' 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<string, string> = {
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 export const TRINITY_FALLBACK_ENGLISH_VOICE = TRINITY_PIPER_VOICE.en
/** Native autonyms / labels for **app UI** locales (General settings); not the full translate menu. */ /** 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' tr: 'Türkçe'
} }
export const PIPER_READ_ALOUD_PROFILE_LABELS: Record<PiperReadAloudProfileCode, string> = {
...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 { export function isTrinityLanguageCode(s: string): s is TrinityLanguageCode {
return TRINITY_SET.has(s as TrinityLanguageCode) return TRINITY_SET.has(s as TrinityLanguageCode)
} }
@ -89,8 +112,8 @@ export type PiperVoiceResolution = {
usedEnglishVoiceFallback: boolean usedEnglishVoiceFallback: boolean
/** True when using another trinity voice (e.g. `zh`) to approximate the requested language. */ /** True when using another trinity voice (e.g. `zh`) to approximate the requested language. */
usedRelatedVoiceFallback: boolean usedRelatedVoiceFallback: boolean
/** Which trinity Piper profile is actually used (native, related, or `en`). */ /** Piper profile for UI labels (trinity locale or e.g. `ar` for Arabic read-aloud). */
piperProfileCode: TrinityLanguageCode piperProfileCode: PiperReadAloudProfileCode
} }
/** /**
@ -113,10 +136,8 @@ const RELATED_PIPER_FOR_BASE: Record<string, TrinityLanguageCode> = {
sk: 'cs', sk: 'cs',
sl: 'de', sl: 'de',
hr: 'ru', hr: 'ru',
pt: 'es',
ca: 'es', ca: 'es',
gl: 'es', gl: 'es',
it: 'es',
ro: 'es', ro: 'es',
la: 'es', la: 'es',
sq: 'es', sq: 'es',
@ -168,6 +189,16 @@ export function getPiperVoiceForChosenLanguage(rawLang: string): PiperVoiceResol
return getPiperVoiceForTrinityLanguage(full) 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 const related = RELATED_PIPER_FOR_BASE[full] ?? RELATED_PIPER_FOR_BASE[base] ?? null
if (related) { if (related) {

Loading…
Cancel
Save