Kokoro: chunk generation, add pre-process func #3412
| @@ -1,5 +1,5 @@ | |||
| 1 | import { debounce_timeout } from '../../constants.js'; | 1 | import { debounce_timeout } from '../../constants.js'; |
| 2 | import { debounceAsync } from '../../utils.js'; | 2 | import { debounceAsync, splitRecursive } from '../../utils.js'; |
| 3 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; | 3 | import { getPreviewString, saveTtsProviderSettings } from './index.js'; |
| 4 | 4 | ||
| 5 | export class KokoroTtsProvider { | 5 | export 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 | } |
| 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 | 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 { | |||
| 258 | 269 | ||
| 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 | } |
| 269 | 284 | ||
| 270 | getVoiceDisplayName(voiceId) { | 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 | 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++; |
| 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 | dispose() { | 345 | dispose() { |
| @@ -1015,6 +1015,10 @@ export function splitRecursive(input, length, delimiters = ['\n\n', '\n', ' ', ' | |||
| 1015 | return result; | 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 | * 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. |