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 @@
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);
275 await new Promise(resolve => {
264 const audioElement = new Audio();276 const audioElement = new Audio();
265 audioElement.src = url;277 audioElement.src = url;
266 audioElement.play();278 audioElement.play();
267 audioElement.onended = () => URL.revokeObjectURL(url);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,7 +320,11 @@ 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;
324 const chunks = splitRecursive(text, chunkSize, ['\n\n', '\n', '.', '?', '!', ',', ' ', '']);
325
326 for (const chunk of chunks) {
327 yield await new Promise((resolve, reject) => {
303 // Store the promise callbacks328 // Store the promise callbacks
304 this.pendingRequests.set(requestId, { resolve, reject });329 this.pendingRequests.set(requestId, { resolve, reject });
305330
@@ -307,7 +332,7 @@ export class KokoroTtsProvider {
307 this.worker.postMessage({332 this.worker.postMessage({
308 action: 'generateTts',333 action: 'generateTts',
309 data: {334 data: {
310 text,335 text: chunk,
311 voice: voice.voice_id,336 voice: voice.voice_id,
312 speakingRate: this.settings.speakingRate || 1.0,337 speakingRate: this.settings.speakingRate || 1.0,
313 requestId,338 requestId,
@@ -315,6 +340,7 @@ export class KokoroTtsProvider {
315 });340 });
316 });341 });
317 }342 }
343 }
318344
319 dispose() {345 dispose() {
320 // Clean up the worker when the provider is disposed346 // 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', ' ', '
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.