diff --git a/apps/readest-app/src/services/tts/EdgeTTSClient.ts b/apps/readest-app/src/services/tts/EdgeTTSClient.ts index af12dd6b..0dc2cef3 100644 --- a/apps/readest-app/src/services/tts/EdgeTTSClient.ts +++ b/apps/readest-app/src/services/tts/EdgeTTSClient.ts @@ -1,15 +1,17 @@ import { getUserLocale } from '@/utils/misc'; import { TTSClient, TTSMessageEvent } from './TTSClient'; import { EdgeSpeechTTS, EdgeTTSPayload, EDGE_TTS_PROTOCOL } from '@/libs/edgeTTS'; +import { TTSGranularity, TTSVoice, TTSVoicesGroup } from './types'; +import { AppService } from '@/types/system'; import { parseSSMLMarks } from '@/utils/ssml'; import { TTSController } from './TTSController'; import { TTSUtils } from './TTSUtils'; -import { TTSGranularity, TTSVoice, TTSVoicesGroup } from './types'; export class EdgeTTSClient implements TTSClient { name = 'edge-tts'; initialized = false; controller?: TTSController; + appService?: AppService | null; #voices: TTSVoice[] = []; #primaryLang = 'en'; @@ -25,8 +27,9 @@ export class EdgeTTSClient implements TTSClient { #startedAt = 0; #fadeCompensation: number | null = null; - constructor(controller?: TTSController) { + constructor(controller?: TTSController, appService?: AppService | null) { this.controller = controller; + this.appService = appService; } async init(protocol: EDGE_TTS_PROTOCOL = 'wss') { @@ -143,11 +146,24 @@ export class EdgeTTSClient implements TTSClient { } as TTSMessageEvent; const result = await new Promise((resolve) => { + let safetyTimeoutId: ReturnType | null = null; const cleanUp = () => { + if (safetyTimeoutId) { + clearTimeout(safetyTimeoutId); + safetyTimeoutId = null; + } audio.onended = null; audio.onerror = null; audio.src = ''; }; + let resolved = false; + const handleEnded = () => { + if (resolved) return; + resolved = true; + cleanUp(); + resolve({ code: 'end', message: `Chunk finished: ${mark.name}` }); + }; + abortHandler = () => { cleanUp(); resolve({ code: 'error', message: 'Aborted' }); @@ -158,9 +174,13 @@ export class EdgeTTSClient implements TTSClient { } else { signal.addEventListener('abort', abortHandler); } - audio.onended = () => { - cleanUp(); - resolve({ code: 'end', message: `Chunk finished: ${mark.name}` }); + audio.onended = handleEnded; + audio.ontimeupdate = () => { + if (!safetyTimeoutId && audio.duration && isFinite(audio.duration)) { + const remainingTime = (audio.duration - audio.currentTime) / this.#rate; + const safetyMargin = 0.5; + safetyTimeoutId = setTimeout(handleEnded, (remainingTime + safetyMargin) * 1000); + } }; audio.onerror = (e) => { cleanUp(); @@ -169,9 +189,16 @@ export class EdgeTTSClient implements TTSClient { }; this.#isPlaying = true; audio.src = audioUrl || ''; + if (!this.appService?.isLinuxApp) { + audio.playbackRate = this.#rate; + } audio .play() - .then(() => (audio.playbackRate = this.#rate)) + .then(() => { + if (this.appService?.isLinuxApp) { + audio.playbackRate = this.#rate; + } + }) .catch((err) => { cleanUp(); console.error('Failed to play audio:', err); diff --git a/apps/readest-app/src/services/tts/TTSController.ts b/apps/readest-app/src/services/tts/TTSController.ts index b2fce063..692bb0ab 100644 --- a/apps/readest-app/src/services/tts/TTSController.ts +++ b/apps/readest-app/src/services/tts/TTSController.ts @@ -53,7 +53,7 @@ export class TTSController extends EventTarget { ) { super(); this.ttsWebClient = new WebSpeechClient(this); - this.ttsEdgeClient = new EdgeTTSClient(this); + this.ttsEdgeClient = new EdgeTTSClient(this, appService); // TODO: implement native TTS client for iOS and PC if (appService?.isAndroidApp) { this.ttsNativeClient = new NativeTTSClient(this);