forked from akai/readest
Preload the next two paragraphs in advance for TTS playback (#151)
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user