From 631279bd3a56261acd571c106638e006ebbf9587 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:10:36 -0700 Subject: [PATCH] TTS: auto-detect Chinese vs English so callers can pass the selection speak()'s lang now defaults to detectLang(text) (any Han/kana -> zh-CN, else en-US) instead of always en-US, so reading a Chinese passage picks the Piper zh voice rather than spelling out characters with the English voice. Explicit lang still overrides. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd --- web/src/audio/speech.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/web/src/audio/speech.ts b/web/src/audio/speech.ts index f540e56..3d7cda8 100644 --- a/web/src/audio/speech.ts +++ b/web/src/audio/speech.ts @@ -59,12 +59,23 @@ function speakWebSpeech(text: string, lang: string): void { synth.speak(utterance) } +// detectLang guesses a BCP-47 locale from the text so the right Piper voice is +// chosen. Any Han/kana character routes to Chinese (zh-CN) — a selection is +// essentially never mixed at the level that matters, and reading a Chinese +// passage with the English voice spells each character out ("Chinese letter…"). +// Everything else defaults to US English, the language she's learning. +const CJK = /[぀-ヿ㐀-䶿一-鿿豈-﫿ヲ-゚]/ +export function detectLang(text: string): string { + return CJK.test(text) ? 'zh-CN' : 'en-US' +} + // speak reads `text` aloud, cancelling anything already in flight so rapid taps -// don't queue up. `lang` defaults to US English (the language she's learning); -// pass a zh locale to hear a Chinese passage. It tries the server's neural voice -// first and silently falls back to the browser voice if that's unavailable (route -// off, network error, or a 404 for a language with no configured voice). -export function speak(text: string, lang = 'en-US'): void { +// don't queue up. `lang` defaults to a guess from the text (Chinese vs English) +// so callers can just pass the selection; pass an explicit locale to override. +// It tries the server's neural voice first and silently falls back to the browser +// voice if that's unavailable (route off, network error, or a 404 for a language +// with no configured voice). +export function speak(text: string, lang = detectLang(text)): void { if (!text.trim()) return stopCurrent() const seq = ++requestSeq