From 1c6cc2bca00e092aa20a5be5e883d51b1250e430 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Tue, 4 Feb 2025 23:44:59 +0100 Subject: [PATCH] tts: change voice language at speak time when the book language is not accurate (#296) --- apps/readest-app/src/services/tts/EdgeTTSClient.ts | 13 +++++++++++-- .../readest-app/src/services/tts/WebSpeechClient.ts | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/readest-app/src/services/tts/EdgeTTSClient.ts b/apps/readest-app/src/services/tts/EdgeTTSClient.ts index ca5ed0fe..d2ec1d10 100644 --- a/apps/readest-app/src/services/tts/EdgeTTSClient.ts +++ b/apps/readest-app/src/services/tts/EdgeTTSClient.ts @@ -8,6 +8,7 @@ export class EdgeTTSClient implements TTSClient { #rate = 1.0; #pitch = 1.0; #voice: TTSVoice | null = null; + #currentVoiceLang = ''; #voices: TTSVoice[] = []; #edgeTTS: EdgeSpeechTTS; @@ -60,12 +61,17 @@ export class EdgeTTSClient implements TTSClient { preload = false, ): AsyncGenerator { const { marks } = parseSSMLMarks(ssml); - const lang = parseSSMLLang(ssml) || 'en'; + const ssmlLang = parseSSMLLang(ssml) || 'en'; + let lang = ssmlLang; + if (lang === 'en' && /[\p{Script=Han}]/u.test(ssml)) { + lang = 'zh'; + } let voiceId = 'en-US-AriaNeural'; - if (!this.#voice) { + if (!this.#voice || ssmlLang !== lang) { const voices = await this.getVoices(lang); this.#voice = voices[0] ? voices[0] : this.#voices.find((v) => v.id === voiceId) || null; + this.#currentVoiceLang = lang; } if (this.#voice) { voiceId = this.#voice.id; @@ -229,6 +235,9 @@ export class EdgeTTSClient implements TTSClient { } async getVoices(lang: string): Promise { + if (this.#currentVoiceLang) { + lang = this.#currentVoiceLang; + } const locale = lang === 'en' ? getUserLocale(lang) || lang : lang; const voices = await this.getAllVoices(); return voices.filter((v) => v.lang.startsWith(locale)); diff --git a/apps/readest-app/src/services/tts/WebSpeechClient.ts b/apps/readest-app/src/services/tts/WebSpeechClient.ts index 93bb98f9..c144eaba 100644 --- a/apps/readest-app/src/services/tts/WebSpeechClient.ts +++ b/apps/readest-app/src/services/tts/WebSpeechClient.ts @@ -175,6 +175,7 @@ export class WebSpeechClient implements TTSClient { #rate = 1.0; #pitch = 1.0; #voice: SpeechSynthesisVoice | null = null; + #currentVoiceLang = ''; #voices: SpeechSynthesisVoice[] = []; #synth = window.speechSynthesis; available = true; @@ -213,11 +214,16 @@ export class WebSpeechClient implements TTSClient { // no need to preload for web speech if (preload) return; - const lang = parseSSMLLang(ssml) || 'en'; - if (!this.#voice) { + const ssmlLang = parseSSMLLang(ssml) || 'en'; + let lang = ssmlLang; + if (lang === 'en' && /[\p{Script=Han}]/u.test(ssml)) { + lang = 'zh'; + } + if (!this.#voice || ssmlLang !== lang) { const voices = await this.getVoices(lang); const voiceId = voices[0]?.id ?? ''; this.#voice = this.#voices.find((v) => v.voiceURI === voiceId) || null; + this.#currentVoiceLang = lang; } for await (const ev of speakWithMarks( ssml, @@ -286,6 +292,9 @@ export class WebSpeechClient implements TTSClient { } async getVoices(lang: string) { + if (this.#currentVoiceLang) { + lang = this.#currentVoiceLang; + } const locale = lang === 'en' ? getUserLocale(lang) || lang : lang; const isValidVoice = (id: string) => { return !id.includes('com.apple') || id.includes('com.apple.voice.compact');