Kokoro: chunk generation, add pre-process func #3412
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | import { debounce_timeout } from '../../constants.js'; |
| 2 | 2 | import { debounceAsync, splitRecursive } from '../../utils.js'; |
| 3 | 3 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; |
| 4 | 4 | |
| 5 | 5 | export class KokoroTtsProvider { |
| @@ -52,6 +52,17 @@ export class KokoroTtsProvider { | ||
| 52 | 52 | this.initTtsDebounced = debounceAsync(this.initializeWorker.bind(this), debounce_timeout.relaxed); |
| 53 | 53 | } |
| 54 | 54 | |
| 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 | 66 | async loadSettings(settings) { |
| 56 | 67 | if (settings.modelId !== undefined) this.settings.modelId = settings.modelId; |
| 57 | 68 | if (settings.dtype !== undefined) this.settings.dtype = settings.dtype; |
| @@ -258,13 +269,17 @@ export class KokoroTtsProvider { | ||
| 258 | 269 | |
| 259 | 270 | const voice = this.getVoice(voiceId); |
| 260 | 271 | const previewText = getPreviewString(voice.lang); |
| 261 | 272 | for await (const response = awaitof this.generateTts(previewText, voiceId);) { |
| 262 | 273 | const audio = await response.blob(); |
| 263 | 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 | } |
| 269 | 284 | |
| 270 | 285 | getVoiceDisplayName(voiceId) { |
| @@ -282,7 +297,13 @@ export class KokoroTtsProvider { | ||
| 282 | 297 | }; |
| 283 | 298 | } |
| 284 | 299 | |
| 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 | 307 | if (!this.ready || !this.worker) { |
| 287 | 308 | console.log('TTS not ready, initializing...'); |
| 288 | 309 | await this.initializeWorker(); |
| @@ -299,21 +320,26 @@ export class KokoroTtsProvider { | ||
| 299 | 320 | const voice = this.getVoice(voiceId); |
| 300 | 321 | const requestId = this.nextRequestId++; |
| 301 | 322 | |
| 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 | + }); | |
| 315 | 341 | }); |
| 316 | 342 | }); |
| 317 | 343 | } |
| 318 | 344 | |
| 319 | 345 | dispose() { |
| @@ -1015,6 +1015,10 @@ export function splitRecursive(input, length, delimiters = ['\n\n', '\n', ' ', ' | ||
| 1015 | 1015 | return result; |
| 1016 | 1016 | } |
| 1017 | 1017 | |
| 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 | + | |
| 1018 | 1022 | /** |
| 1019 | 1023 | * Checks if a string is a valid data URL. |
| 1020 | 1024 | * @param {string} str The string to check. |