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 @@
1import { debounce_timeout } from '../../constants.js';1import { debounce_timeout } from '../../constants.js';
2import { debounceAsync } from '../../utils.js';2import { debounceAsync, splitRecursive } from '../../utils.js';
3import { getPreviewString, saveTtsProviderSettings } from './index.js';3import { getPreviewString, saveTtsProviderSettings } from './index.js';
44
5export class KokoroTtsProvider {5export class KokoroTtsProvider {
@@ -52,6 +52,17 @@ export class KokoroTtsProvider {
52 this.initTtsDebounced = debounceAsync(this.initializeWorker.bind(this), debounce_timeout.relaxed);52 this.initTtsDebounced = debounceAsync(this.initializeWorker.bind(this), debounce_timeout.relaxed);
53 }53 }
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
55 async loadSettings(settings) {66 async loadSettings(settings) {
56 if (settings.modelId !== undefined) this.settings.modelId = settings.modelId;67 if (settings.modelId !== undefined) this.settings.modelId = settings.modelId;
57 if (settings.dtype !== undefined) this.settings.dtype = settings.dtype;68 if (settings.dtype !== undefined) this.settings.dtype = settings.dtype;
@@ -258,13 +269,17 @@ export class KokoroTtsProvider {
258269
259 const voice = this.getVoice(voiceId);270 const voice = this.getVoice(voiceId);
260 const previewText = getPreviewString(voice.lang);271 const previewText = getPreviewString(voice.lang);
261 const response = await this.generateTts(previewText, voiceId);272 for await (const response of this.generateTts(previewText, voiceId)) {
262 const audio = await response.blob();273 const audio = await response.blob();
263 const url = URL.createObjectURL(audio);274 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 }
268 }283 }
269284
270 getVoiceDisplayName(voiceId) {285 getVoiceDisplayName(voiceId) {
@@ -282,7 +297,13 @@ export class KokoroTtsProvider {
282 };297 };
283 }298 }
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) {
286 if (!this.ready || !this.worker) {307 if (!this.ready || !this.worker) {
287 console.log('TTS not ready, initializing...');308 console.log('TTS not ready, initializing...');
288 await this.initializeWorker();309 await this.initializeWorker();
@@ -299,21 +320,26 @@ export class KokoroTtsProvider {
299 const voice = this.getVoice(voiceId);320 const voice = this.getVoice(voiceId);
300 const requestId = this.nextRequestId++;321 const requestId = this.nextRequestId++;
301322
302 return new Promise((resolve, reject) => {323 const chunkSize = 400;
303 // Store the promise callbacks324 const chunks = splitRecursive(text, chunkSize, ['\n\n', '\n', '.', '?', '!', ',', ' ', '']);
304 this.pendingRequests.set(requestId, { resolve, reject });325
305326 for (const chunk of chunks) {
306 // Send the request to the worker327 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 });
315 });341 });
316 });342 }
317 }343 }
318344
319 dispose() {345 dispose() {
public/scripts/utils.js+4 -0
@@ -1015,6 +1015,10 @@ export function splitRecursive(input, length, delimiters = ['\n\n', '\n', ' ', '
1015 return result;1015 return result;
1016}1016}
10171017
1018export 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
1018/**1022/**
1019 * Checks if a string is a valid data URL.1023 * Checks if a string is a valid data URL.
1020 * @param {string} str The string to check.1024 * @param {string} str The string to check.