Kokoro: chunk generation, add pre-process func #3412

1b817cd89747f2d1ffda68dfe63d36c25f777395

Cohee <18619528+Cohee1207@users.noreply.github.com>

2 files changed, +53 -23Ignore whitespace
public/scripts/extensions/tts/kokoro.js+49 -23
@@ -1,5 +1,5 @@
11import { debounce_timeout } from '../../constants.js';
22import { debounceAsync, splitRecursive } from '../../utils.js';
33import { getPreviewString, saveTtsProviderSettings } from './index.js';
44
55export class KokoroTtsProvider {
@@ -52,6 +52,17 @@ export class KokoroTtsProvider {
5252 this.initTtsDebounced = debounceAsync(this.initializeWorker.bind(this), debounce_timeout.relaxed);
5353 }
5454
55+ /**
56+ * Perform any text processing before passing to TTS engine.
57+ * @param {string} text Input text
58+ * @returns {string} Processed text
59+ */
60+ processText(text) {
61+ // TILDE!
62+ text = text.replace(/~/g, '.');
63+ return text;
64+ }
65+
5566 async loadSettings(settings) {
5667 if (settings.modelId !== undefined) this.settings.modelId = settings.modelId;
5768 if (settings.dtype !== undefined) this.settings.dtype = settings.dtype;
@@ -258,13 +269,17 @@ export class KokoroTtsProvider {
258269
259270 const voice = this.getVoice(voiceId);
260271 const previewText = getPreviewString(voice.lang);
261272 for await (const response = awaitof this.generateTts(previewText, voiceId);) {
262273 const audio = await response.blob();
263274 const url = URL.createObjectURL(audio);
264- const audioElement = new Audio();
275+ await new Promise(resolve => {
265- audioElement.src = url;
276+ const audioElement = new Audio();
266- audioElement.play();
277+ audioElement.src = url;
267- audioElement.onended = () => URL.revokeObjectURL(url);
278+ audioElement.play();
279+ audioElement.onended = () => resolve();
280+ });
281+ URL.revokeObjectURL(url);
282+ }
268283 }
269284
270285 getVoiceDisplayName(voiceId) {
@@ -282,7 +297,13 @@ export class KokoroTtsProvider {
282297 };
283298 }
284299
285- async generateTts(text, voiceId) {
300+ /**
301+ * Generate TTS audio for the given text using the specified voice.
302+ * @param {string} text Text to generate
303+ * @param {string} voiceId Voice ID
304+ * @returns {AsyncGenerator<Response>} Audio response generator
305+ */
306+ async* generateTts(text, voiceId) {
286307 if (!this.ready || !this.worker) {
287308 console.log('TTS not ready, initializing...');
288309 await this.initializeWorker();
@@ -299,21 +320,26 @@ export class KokoroTtsProvider {
299320 const voice = this.getVoice(voiceId);
300321 const requestId = this.nextRequestId++;
301322
302- return new Promise((resolve, reject) => {
323+ const chunkSize = 400;
303- // Store the promise callbacks
324+ const chunks = splitRecursive(text, chunkSize, ['\n\n', '\n', '.', '?', '!', ',', ' ', '']);
304- this.pendingRequests.set(requestId, { resolve, reject });
325+
305-
326+ for (const chunk of chunks) {
306- // Send the request to the worker
327+ yield await new Promise((resolve, reject) => {
307- this.worker.postMessage({
328+ // Store the promise callbacks
308- action: 'generateTts',
329+ this.pendingRequests.set(requestId, { resolve, reject });
309- data: {
330+
310- text,
331+ // Send the request to the worker
311- voice: voice.voice_id,
332+ this.worker.postMessage({
312- speakingRate: this.settings.speakingRate || 1.0,
333+ action: 'generateTts',
313- requestId,
334+ data: {
314- },
335+ text: chunk,
336+ voice: voice.voice_id,
337+ speakingRate: this.settings.speakingRate || 1.0,
338+ requestId,
339+ },
340+ });
315341 });
316342 });
317343 }
318344
319345 dispose() {
public/scripts/utils.js+4 -0
@@ -1015,6 +1015,10 @@ export function splitRecursive(input, length, delimiters = ['\n\n', '\n', ' ', '
10151015 return result;
10161016}
10171017
1018+export function splitSentences(input, length) {
1019+ var pattRegex = new RegExp(`^[\\s\\S]{${Math.floor(length / 2)},${length}}[.!?,]{1}|^[\\s\\S]{1,${length}}$|^[\\s\\S]{1,${length}}`);
1020+}
1021+
10181022/**
10191023 * Checks if a string is a valid data URL.
10201024 * @param {string} str The string to check.