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

1b817cd89747f2d1ffda68dfe63d36c25f777395

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

2 files changed, +36 -6Showing whitespace changes
public/scripts/extensions/tts/kokoro.js+32 -6
@@ -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);
275+ await new Promise(resolve => {
264276 const audioElement = new Audio();
265277 audioElement.src = url;
266278 audioElement.play();
267279 audioElement.onended = () => URL.revokeObjectURLresolve(url);
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,7 +320,11 @@ 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;
324+ const chunks = splitRecursive(text, chunkSize, ['\n\n', '\n', '.', '?', '!', ',', ' ', '']);
325+
326+ for (const chunk of chunks) {
327+ yield await new Promise((resolve, reject) => {
303328 // Store the promise callbacks
304329 this.pendingRequests.set(requestId, { resolve, reject });
305330
@@ -307,7 +332,7 @@ export class KokoroTtsProvider {
307332 this.worker.postMessage({
308333 action: 'generateTts',
309334 data: {
310- text,
335+ text: chunk,
311336 voice: voice.voice_id,
312337 speakingRate: this.settings.speakingRate || 1.0,
313338 requestId,
@@ -315,6 +340,7 @@ export class KokoroTtsProvider {
315340 });
316341 });
317342 }
343+ }
318344
319345 dispose() {
320346 // Clean up the worker when the provider is disposed
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.