Preload the next two paragraphs in advance for TTS playback (#151)

This commit is contained in:
Huang Xin
2025-01-14 01:45:54 +01:00
committed by GitHub
parent 211fc2cd22
commit 7e9ddcd7cc
4 changed files with 59 additions and 14 deletions
@@ -44,7 +44,11 @@ export class EdgeTTSClient implements TTSClient {
return { lang, text, voice: voiceId, rate: this.#rate, pitch: this.#pitch } as EdgeTTSPayload;
};
async *speak(ssml: string, signal: AbortSignal): AsyncGenerator<TTSMessageEvent> {
async *speak(
ssml: string,
signal: AbortSignal,
preload = false,
): AsyncGenerator<TTSMessageEvent> {
const { marks } = parseSSMLMarks(ssml);
const lang = parseSSMLLang(ssml) || 'en';
@@ -57,15 +61,19 @@ export class EdgeTTSClient implements TTSClient {
voiceId = this.#voice.id;
}
await this.stopInternal();
// Preloading for longer ssml
if (marks.length > 1) {
for (const mark of marks.slice(1)) {
this.#edgeTTS.createAudio(this.getPayload(lang, mark.text, voiceId)).catch((error) => {
console.warn('Error preloading mark:', mark, error);
if (preload) {
for (const mark of marks) {
await this.#edgeTTS.createAudio(this.getPayload(lang, mark.text, voiceId)).catch((err) => {
console.warn('Error preloading:', err);
});
}
yield {
code: 'end',
message: 'Preload finished',
};
return;
} else {
await this.stopInternal();
}
for (const mark of marks) {
@@ -102,6 +110,13 @@ export class EdgeTTSClient implements TTSClient {
message: `Chunk finished: ${mark.name}`,
});
};
if (signal.aborted) {
resolve({
code: 'error',
message: 'Aborted',
});
return;
}
this.#sourceNode.start(0);
this.#isPlaying = true;
this.#startedAt = this.#audioContext.currentTime;
@@ -17,7 +17,7 @@ export interface TTSVoice {
export interface TTSClient {
init(): Promise<boolean>;
speak(ssml: string, signal: AbortSignal): AsyncIterable<TTSMessageEvent>;
speak(ssml: string, signal: AbortSignal, preload?: boolean): AsyncIterable<TTSMessageEvent>;
pause(): Promise<void>;
resume(): Promise<void>;
stop(): Promise<void>;
@@ -54,6 +54,27 @@ export class TTSController extends EventTarget {
await this.view.initTTS(granularity);
}
async preloadSSML(ssml: string | undefined) {
if (!ssml) return;
const iter = await this.ttsClient.speak(ssml, new AbortController().signal, true);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const _ of iter);
}
async preloadNextSSML(count: number) {
const tts = this.view.tts;
if (!tts) return;
let preloaded = 0;
for (let i = 0; i < count; i++) {
const ssml = tts.next();
this.preloadSSML(ssml);
if (ssml) preloaded++;
}
for (let i = 0; i < preloaded; i++) {
tts.prev();
}
}
async #speak(ssml: string | undefined | Promise<string>) {
await this.stop();
this.#currentSpeakAbortController = new AbortController();
@@ -63,6 +84,7 @@ export class TTSController extends EventTarget {
console.log('TTS speak');
this.state = 'playing';
ssml = await ssml;
await this.preloadSSML(ssml);
if (!ssml) {
this.#nossmlCnt++;
// FIXME: in case we are at the end of the book, need a better way to handle this
@@ -95,6 +117,7 @@ export class TTSController extends EventTarget {
async speak(ssml: string | Promise<string>) {
await this.initViewTTS();
this.#speak(ssml).catch((e) => this.error(e));
this.preloadNextSSML(2);
}
play() {
@@ -107,10 +130,9 @@ export class TTSController extends EventTarget {
async start() {
await this.initViewTTS();
const resumeOrStart = this.state.includes('paused')
? this.view.tts?.resume()
: this.view.tts?.start();
return this.#speak(resumeOrStart);
const ssml = this.state.includes('paused') ? this.view.tts?.resume() : this.view.tts?.start();
this.#speak(ssml);
this.preloadNextSSML(2);
}
async pause() {
@@ -149,6 +171,7 @@ export class TTSController extends EventTarget {
if (this.state === 'playing') {
await this.stop();
this.#speak(this.view.tts?.next());
this.preloadNextSSML(2);
} else {
this.state = 'forward-paused';
this.view.tts?.next(true);
@@ -205,7 +205,14 @@ export class WebSpeechClient implements TTSClient {
return this.available;
}
async *speak(ssml: string, signal: AbortSignal): AsyncGenerator<TTSMessageEvent> {
async *speak(
ssml: string,
signal: AbortSignal,
preload = false,
): AsyncGenerator<TTSMessageEvent> {
// no need to preload for web speech
if (preload) return;
const lang = parseSSMLLang(ssml) || 'en';
if (!this.#voice) {
const voices = await this.getVoices(lang);