fix(tts): set playback rate after play only on Linux (#3040)

This commit is contained in:
Huang Xin
2026-01-23 12:14:13 +01:00
committed by GitHub
parent b1153ba051
commit 481d8198e9
2 changed files with 34 additions and 7 deletions
@@ -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<TTSMessageEvent>((resolve) => {
let safetyTimeoutId: ReturnType<typeof setTimeout> | 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);
@@ -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);