| 1 | /* |
| 2 | TODO: |
| 3 | - Hide voice map its just confusing |
| 4 | - Delete useless call |
| 5 | */ |
| 6 | |
| 7 | import { doExtrasFetch, extension_settings, getApiUrl, modules } from '../../extensions.js'; |
| 8 | import { initVoiceMap } from './index.js'; |
| 9 | import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; |
| 10 | |
| 11 | export { CoquiTtsProvider }; |
| 12 | |
| 13 | const DEBUG_PREFIX = '<Coqui TTS module> '; |
| 14 | |
| 15 | let inApiCall = false; |
| 16 | let coquiApiModels = {}; // Initialized only once |
| 17 | let coquiApiModelsFull = {}; // Initialized only once |
| 18 | let coquiLocalModels = []; // Initialized only once |
| 19 | let coquiLocalModelsReceived = false; |
| 20 | /* |
| 21 | coquiApiModels format [language][dataset][name]:coqui-api-model-id, example: |
| 22 | { |
| 23 | "en": { |
| 24 | "vctk": { |
| 25 | "vits": "tts_models/en/vctk/vits" |
| 26 | } |
| 27 | }, |
| 28 | "ja": { |
| 29 | "kokoro": { |
| 30 | "tacotron2-DDC": "tts_models/ja/kokoro/tacotron2-DDC" |
| 31 | } |
| 32 | } |
| 33 | } |
| 34 | */ |
| 35 | const languageLabels = { |
| 36 | 'multilingual': 'Multilingual', |
| 37 | 'en': 'English', |
| 38 | 'fr': 'French', |
| 39 | 'es': 'Spanish', |
| 40 | 'ja': 'Japanese', |
| 41 | }; |
| 42 | |
| 43 | function throwIfModuleMissing() { |
| 44 | if (!modules.includes('coqui-tts')) { |
| 45 | const message = 'Coqui TTS module not loaded. Add coqui-tts to enable-modules and restart the Extras API.'; |
| 46 | // toastr.error(message, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 47 | throw new Error(DEBUG_PREFIX, message); |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | function resetModelSettings() { |
| 52 | $('#coqui_api_model_settings_language').val('none'); |
| 53 | $('#coqui_api_model_settings_speaker').val('none'); |
| 54 | } |
| 55 | |
| 56 | class CoquiTtsProvider { |
| 57 | //#############################// |
| 58 | // Extension UI and Settings // |
| 59 | //#############################// |
| 60 | |
| 61 | settings; |
| 62 | |
| 63 | defaultSettings = { |
| 64 | voiceMap: {}, |
| 65 | customVoices: {}, |
| 66 | voiceIds: [], |
| 67 | voiceMapDict: {}, |
| 68 | }; |
| 69 | |
| 70 | get settingsHtml() { |
| 71 | let html = ` |
| 72 | <div class="flex wide100p flexGap10 alignitemscenter"> |
| 73 | <div> |
| 74 | <div style="flex: 50%;"> |
| 75 | <small>To use CoquiTTS, select the origin, language, and model, then click Add Voice. The voice will then be available to add to a character. Voices are saved globally. </small><br> |
| 76 | <label for="coqui_voicename_select">Select Saved Voice:</label> |
| 77 | <select id="coqui_voicename_select"> |
| 78 | <!-- Populated by JS --> |
| 79 | </select> |
| 80 | <div class="tts_block"> |
| 81 | <input id="coqui_remove_voiceId_mapping" class="menu_button" type="button" value="Remove Voice" /> |
| 82 | <input id="coqui_add_voiceId_mapping" class="menu_button" type="button" value="Add Voice" /> |
| 83 | </div> |
| 84 | <label for="coqui_model_origin">Models:</label> |
| 85 | <select id="coqui_model_origin">gpu_mode |
| 86 | <option value="none">Select Origin</option> |
| 87 | <option value="coqui-api">Coqui API (Tested)</option> |
| 88 | <option value="coqui-api-full">Coqui API (Experimental)</option> |
| 89 | <option value="local">My Models</option> |
| 90 | </select> |
| 91 | |
| 92 | <div id="coqui_api_model_div"> |
| 93 | <select id="coqui_api_language"> |
| 94 | <!-- Populated by JS and request --> |
| 95 | </select> |
| 96 | |
| 97 | <select id="coqui_api_model_name"> |
| 98 | <!-- Populated by JS and request --> |
| 99 | </select> |
| 100 | |
| 101 | <div id="coqui_api_model_settings"> |
| 102 | <select id="coqui_api_model_settings_language"> |
| 103 | <!-- Populated by JS and request --> |
| 104 | </select> |
| 105 | <select id="coqui_api_model_settings_speaker"> |
| 106 | <!-- Populated by JS and request --> |
| 107 | </select> |
| 108 | </div> |
| 109 | <span id="coqui_api_model_install_status">Model installed on extras server</span> |
| 110 | <input id="coqui_api_model_install_button" class="menu_button" type="button" value="Install" /> |
| 111 | </div> |
| 112 | |
| 113 | <div id="coqui_local_model_div"> |
| 114 | <select id="coqui_local_model_name"> |
| 115 | <!-- Populated by JS and request --> |
| 116 | </select> |
| 117 | </div> |
| 118 | |
| 119 | </div> |
| 120 | </div> |
| 121 | </div> |
| 122 | `; |
| 123 | return html; |
| 124 | } |
| 125 | |
| 126 | async loadSettings(settings) { |
| 127 | // Only accept keys defined in defaultSettings |
| 128 | this.settings = this.defaultSettings; |
| 129 | |
| 130 | for (const key in settings) { |
| 131 | if (key in this.settings) { |
| 132 | this.settings[key] = settings[key]; |
| 133 | } else { |
| 134 | throw DEBUG_PREFIX + `Invalid setting passed to extension: ${key}`; |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | await initLocalModels(); |
| 139 | this.updateCustomVoices(); // Overide any manual modification |
| 140 | |
| 141 | $('#coqui_api_model_div').hide(); |
| 142 | $('#coqui_local_model_div').hide(); |
| 143 | |
| 144 | $('#coqui_api_language').show(); |
| 145 | $('#coqui_api_model_name').hide(); |
| 146 | $('#coqui_api_model_settings').hide(); |
| 147 | $('#coqui_api_model_install_status').hide(); |
| 148 | $('#coqui_api_model_install_button').hide(); |
| 149 | |
| 150 | let that = this; |
| 151 | $('#coqui_model_origin').on('change', function () { that.onModelOriginChange(); }); |
| 152 | $('#coqui_api_language').on('change', function () { that.onModelLanguageChange(); }); |
| 153 | $('#coqui_api_model_name').on('change', function () { that.onModelNameChange(); }); |
| 154 | |
| 155 | $('#coqui_remove_voiceId_mapping').on('click', function () { that.onRemoveClick(); }); |
| 156 | $('#coqui_add_voiceId_mapping').on('click', function () { that.onAddClick(); }); |
| 157 | |
| 158 | // Load coqui-api settings from json file |
| 159 | await fetch('/scripts/extensions/tts/coqui_api_models_settings.json') |
| 160 | .then(response => response.json()) |
| 161 | .then(json => { |
| 162 | coquiApiModels = json; |
| 163 | console.debug(DEBUG_PREFIX, 'initialized coqui-api model list to', coquiApiModels); |
| 164 | /* |
| 165 | $('#coqui_api_language') |
| 166 | .find('option') |
| 167 | .remove() |
| 168 | .end() |
| 169 | .append('<option value="none">Select model language</option>') |
| 170 | .val('none'); |
| 171 | |
| 172 | for(let language in coquiApiModels) { |
| 173 | $("#coqui_api_language").append(new Option(languageLabels[language],language)); |
| 174 | console.log(DEBUG_PREFIX,"added language",language); |
| 175 | }*/ |
| 176 | }); |
| 177 | |
| 178 | // Load coqui-api FULL settings from json file |
| 179 | await fetch('/scripts/extensions/tts/coqui_api_models_settings_full.json') |
| 180 | .then(response => response.json()) |
| 181 | .then(json => { |
| 182 | coquiApiModelsFull = json; |
| 183 | console.debug(DEBUG_PREFIX, 'initialized coqui-api full model list to', coquiApiModelsFull); |
| 184 | /* |
| 185 | $('#coqui_api_full_language') |
| 186 | .find('option') |
| 187 | .remove() |
| 188 | .end() |
| 189 | .append('<option value="none">Select model language</option>') |
| 190 | .val('none'); |
| 191 | |
| 192 | for(let language in coquiApiModelsFull) { |
| 193 | $("#coqui_api_full_language").append(new Option(languageLabels[language],language)); |
| 194 | console.log(DEBUG_PREFIX,"added language",language); |
| 195 | }*/ |
| 196 | }); |
| 197 | } |
| 198 | |
| 199 | // Perform a simple readiness check by trying to fetch voiceIds |
| 200 | async checkReady() { |
| 201 | throwIfModuleMissing(); |
| 202 | await this.fetchTtsVoiceObjects(); |
| 203 | } |
| 204 | |
| 205 | updateCustomVoices() { |
| 206 | // Takes voiceMapDict and converts it to a string to save to voiceMap |
| 207 | this.settings.customVoices = {}; |
| 208 | for (let voiceName in this.settings.voiceMapDict) { |
| 209 | const voiceId = this.settings.voiceMapDict[voiceName]; |
| 210 | this.settings.customVoices[voiceName] = voiceId.model_id; |
| 211 | |
| 212 | if (voiceId.model_language != null) |
| 213 | this.settings.customVoices[voiceName] += '[' + voiceId.model_language + ']'; |
| 214 | |
| 215 | if (voiceId.model_speaker != null) |
| 216 | this.settings.customVoices[voiceName] += '[' + voiceId.model_speaker + ']'; |
| 217 | } |
| 218 | |
| 219 | // Update UI select list with voices |
| 220 | $('#coqui_voicename_select').empty(); |
| 221 | $('#coqui_voicename_select') |
| 222 | .find('option') |
| 223 | .remove() |
| 224 | .end() |
| 225 | .append('<option value="none">Select Voice</option>') |
| 226 | .val('none'); |
| 227 | for (const voiceName in this.settings.voiceMapDict) { |
| 228 | $('#coqui_voicename_select').append(new Option(voiceName, voiceName)); |
| 229 | } |
| 230 | |
| 231 | this.onSettingsChange(); |
| 232 | } |
| 233 | |
| 234 | onSettingsChange() { |
| 235 | console.debug(DEBUG_PREFIX, 'Settings changes', this.settings); |
| 236 | extension_settings.tts.Coqui = this.settings; |
| 237 | } |
| 238 | |
| 239 | async onRefreshClick() { |
| 240 | this.checkReady(); |
| 241 | } |
| 242 | |
| 243 | async onAddClick() { |
| 244 | if (inApiCall) { |
| 245 | return; //TODO: block dropdown |
| 246 | } |
| 247 | |
| 248 | // Ask user for voiceId name to save voice |
| 249 | const voiceName = await callGenericPopup('Name of Coqui voice to add to voice select dropdown:', POPUP_TYPE.INPUT); |
| 250 | |
| 251 | const model_origin = $('#coqui_model_origin').val(); |
| 252 | const model_language = $('#coqui_api_language').val(); |
| 253 | const model_name = $('#coqui_api_model_name').val(); |
| 254 | let model_setting_language = $('#coqui_api_model_settings_language').val(); |
| 255 | let model_setting_speaker = $('#coqui_api_model_settings_speaker').val(); |
| 256 | |
| 257 | |
| 258 | if (!voiceName) { |
| 259 | toastr.error('Voice name empty, please enter one.', DEBUG_PREFIX + ' voice mapping voice name', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 260 | this.updateCustomVoices(); // Overide any manual modification |
| 261 | return; |
| 262 | } |
| 263 | |
| 264 | if (model_origin == 'none') { |
| 265 | toastr.error('Origin not selected, please select one.', DEBUG_PREFIX + ' voice mapping origin', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 266 | this.updateCustomVoices(); // Overide any manual modification |
| 267 | return; |
| 268 | } |
| 269 | |
| 270 | if (model_origin == 'local') { |
| 271 | const model_id = $('#coqui_local_model_name').val(); |
| 272 | |
| 273 | if (model_name == 'none') { |
| 274 | toastr.error('Model not selected, please select one.', DEBUG_PREFIX + ' voice mapping model', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 275 | this.updateCustomVoices(); // Overide any manual modification |
| 276 | return; |
| 277 | } |
| 278 | |
| 279 | this.settings.voiceMapDict[voiceName] = { model_type: 'local', model_id: 'local/' + model_id }; |
| 280 | console.debug(DEBUG_PREFIX, 'Registered new voice map: ', voiceName, ':', this.settings.voiceMapDict[voiceName]); |
| 281 | this.updateCustomVoices(); // Overide any manual modification |
| 282 | return; |
| 283 | } |
| 284 | |
| 285 | if (model_language == 'none') { |
| 286 | toastr.error('Language not selected, please select one.', DEBUG_PREFIX + ' voice mapping language', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 287 | this.updateCustomVoices(); // Overide any manual modification |
| 288 | return; |
| 289 | } |
| 290 | |
| 291 | if (model_name == 'none') { |
| 292 | toastr.error('Model not selected, please select one.', DEBUG_PREFIX + ' voice mapping model', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 293 | this.updateCustomVoices(); // Overide any manual modification |
| 294 | return; |
| 295 | } |
| 296 | |
| 297 | if (model_setting_language == 'none') |
| 298 | model_setting_language = null; |
| 299 | |
| 300 | if (model_setting_speaker == 'none') |
| 301 | model_setting_speaker = null; |
| 302 | |
| 303 | const tokens = $('#coqui_api_model_name').val().split('/'); |
| 304 | const model_dataset = tokens[0]; |
| 305 | const model_label = tokens[1]; |
| 306 | const model_id = 'tts_models/' + model_language + '/' + model_dataset + '/' + model_label; |
| 307 | |
| 308 | let modelDict = coquiApiModels; |
| 309 | if (model_origin == 'coqui-api-full') |
| 310 | modelDict = coquiApiModelsFull; |
| 311 | |
| 312 | if (model_setting_language == null & 'languages' in modelDict[model_language][model_dataset][model_label]) { |
| 313 | toastr.error('Model language not selected, please select one.', DEBUG_PREFIX + ' voice mapping model language', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 314 | return; |
| 315 | } |
| 316 | |
| 317 | if (model_setting_speaker == null & 'speakers' in modelDict[model_language][model_dataset][model_label]) { |
| 318 | toastr.error('Model speaker not selected, please select one.', DEBUG_PREFIX + ' voice mapping model speaker', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 319 | return; |
| 320 | } |
| 321 | |
| 322 | console.debug(DEBUG_PREFIX, 'Current custom voices: ', this.settings.customVoices); |
| 323 | |
| 324 | this.settings.voiceMapDict[voiceName] = { model_type: 'coqui-api', model_id: model_id, model_language: model_setting_language, model_speaker: model_setting_speaker }; |
| 325 | |
| 326 | console.debug(DEBUG_PREFIX, 'Registered new voice map: ', voiceName, ':', this.settings.voiceMapDict[voiceName]); |
| 327 | |
| 328 | this.updateCustomVoices(); |
| 329 | initVoiceMap(); // Update TTS extension voiceMap |
| 330 | |
| 331 | let successMsg = voiceName + ':' + model_id; |
| 332 | if (model_setting_language != null) |
| 333 | successMsg += '[' + model_setting_language + ']'; |
| 334 | if (model_setting_speaker != null) |
| 335 | successMsg += '[' + model_setting_speaker + ']'; |
| 336 | toastr.info(successMsg, DEBUG_PREFIX + ' voice map updated', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 337 | |
| 338 | return; |
| 339 | } |
| 340 | |
| 341 | async getVoice(voiceName) { |
| 342 | let match = await this.fetchTtsVoiceObjects(); |
| 343 | match = match.filter( |
| 344 | voice => voice.name == voiceName, |
| 345 | )[0]; |
| 346 | if (!match) { |
| 347 | throw `TTS Voice name ${voiceName} not found in CoquiTTS Provider voice list`; |
| 348 | } |
| 349 | return match; |
| 350 | } |
| 351 | |
| 352 | async onRemoveClick() { |
| 353 | const voiceName = $('#coqui_voicename_select').val(); |
| 354 | |
| 355 | if (voiceName === 'none') { |
| 356 | toastr.error('Voice not selected, please select one.', DEBUG_PREFIX + ' voice mapping voiceId', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 357 | return; |
| 358 | } |
| 359 | |
| 360 | // Todo erase from voicemap |
| 361 | delete (this.settings.voiceMapDict[voiceName]); |
| 362 | this.updateCustomVoices(); |
| 363 | initVoiceMap(); // Update TTS extension voiceMap |
| 364 | } |
| 365 | |
| 366 | async onModelOriginChange() { |
| 367 | throwIfModuleMissing(); |
| 368 | resetModelSettings(); |
| 369 | const model_origin = $('#coqui_model_origin').val(); |
| 370 | |
| 371 | if (model_origin == 'none') { |
| 372 | $('#coqui_local_model_div').hide(); |
| 373 | $('#coqui_api_model_div').hide(); |
| 374 | } |
| 375 | |
| 376 | // show coqui model selected list (SAFE) |
| 377 | if (model_origin == 'coqui-api') { |
| 378 | $('#coqui_local_model_div').hide(); |
| 379 | |
| 380 | $('#coqui_api_language') |
| 381 | .find('option') |
| 382 | .remove() |
| 383 | .end() |
| 384 | .append('<option value="none">Select model language</option>') |
| 385 | .val('none'); |
| 386 | |
| 387 | for (let language in coquiApiModels) { |
| 388 | let languageLabel = language; |
| 389 | if (language in languageLabels) |
| 390 | languageLabel = languageLabels[language]; |
| 391 | $('#coqui_api_language').append(new Option(languageLabel, language)); |
| 392 | console.log(DEBUG_PREFIX, 'added language', languageLabel, '(', language, ')'); |
| 393 | } |
| 394 | |
| 395 | $('#coqui_api_model_div').show(); |
| 396 | } |
| 397 | |
| 398 | // show coqui model full list (UNSAFE) |
| 399 | if (model_origin == 'coqui-api-full') { |
| 400 | $('#coqui_local_model_div').hide(); |
| 401 | |
| 402 | $('#coqui_api_language') |
| 403 | .find('option') |
| 404 | .remove() |
| 405 | .end() |
| 406 | .append('<option value="none">Select model language</option>') |
| 407 | .val('none'); |
| 408 | |
| 409 | for (let language in coquiApiModelsFull) { |
| 410 | let languageLabel = language; |
| 411 | if (language in languageLabels) |
| 412 | languageLabel = languageLabels[language]; |
| 413 | $('#coqui_api_language').append(new Option(languageLabel, language)); |
| 414 | console.log(DEBUG_PREFIX, 'added language', languageLabel, '(', language, ')'); |
| 415 | } |
| 416 | |
| 417 | $('#coqui_api_model_div').show(); |
| 418 | } |
| 419 | |
| 420 | |
| 421 | // show local model list |
| 422 | if (model_origin == 'local') { |
| 423 | $('#coqui_api_model_div').hide(); |
| 424 | $('#coqui_local_model_div').show(); |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | async onModelLanguageChange() { |
| 429 | throwIfModuleMissing(); |
| 430 | resetModelSettings(); |
| 431 | $('#coqui_api_model_settings').hide(); |
| 432 | const model_origin = $('#coqui_model_origin').val(); |
| 433 | const model_language = $('#coqui_api_language').val(); |
| 434 | console.debug(model_language); |
| 435 | |
| 436 | if (model_language == 'none') { |
| 437 | $('#coqui_api_model_name').hide(); |
| 438 | return; |
| 439 | } |
| 440 | |
| 441 | $('#coqui_api_model_name').show(); |
| 442 | $('#coqui_api_model_name') |
| 443 | .find('option') |
| 444 | .remove() |
| 445 | .end() |
| 446 | .append('<option value="none">Select model</option>') |
| 447 | .val('none'); |
| 448 | |
| 449 | let modelDict = coquiApiModels; |
| 450 | if (model_origin == 'coqui-api-full') |
| 451 | modelDict = coquiApiModelsFull; |
| 452 | |
| 453 | for (let model_dataset in modelDict[model_language]) |
| 454 | for (let model_name in modelDict[model_language][model_dataset]) { |
| 455 | const model_id = model_dataset + '/' + model_name; |
| 456 | const model_label = model_name + ' (' + model_dataset + ' dataset)'; |
| 457 | $('#coqui_api_model_name').append(new Option(model_label, model_id)); |
| 458 | } |
| 459 | } |
| 460 | |
| 461 | async onModelNameChange() { |
| 462 | throwIfModuleMissing(); |
| 463 | resetModelSettings(); |
| 464 | $('#coqui_api_model_settings').hide(); |
| 465 | const model_origin = $('#coqui_model_origin').val(); |
| 466 | |
| 467 | // No model selected |
| 468 | if ($('#coqui_api_model_name').val() == 'none') { |
| 469 | $('#coqui_api_model_install_button').off('click'); |
| 470 | $('#coqui_api_model_install_button').hide(); |
| 471 | return; |
| 472 | } |
| 473 | |
| 474 | // Get languages and speakers options |
| 475 | const model_language = $('#coqui_api_language').val(); |
| 476 | const tokens = $('#coqui_api_model_name').val().split('/'); |
| 477 | const model_dataset = tokens[0]; |
| 478 | const model_name = tokens[1]; |
| 479 | |
| 480 | let modelDict = coquiApiModels; |
| 481 | if (model_origin == 'coqui-api-full') |
| 482 | modelDict = coquiApiModelsFull; |
| 483 | |
| 484 | const model_settings = modelDict[model_language][model_dataset][model_name]; |
| 485 | |
| 486 | if ('languages' in model_settings) { |
| 487 | $('#coqui_api_model_settings').show(); |
| 488 | $('#coqui_api_model_settings_language').show(); |
| 489 | $('#coqui_api_model_settings_language') |
| 490 | .find('option') |
| 491 | .remove() |
| 492 | .end() |
| 493 | .append('<option value="none">Select language</option>') |
| 494 | .val('none'); |
| 495 | |
| 496 | for (let i = 0; i < model_settings.languages.length; i++) { |
| 497 | const language_label = JSON.stringify(model_settings.languages[i]).replaceAll('"', ''); |
| 498 | $('#coqui_api_model_settings_language').append(new Option(language_label, i)); |
| 499 | } |
| 500 | } else { |
| 501 | $('#coqui_api_model_settings_language').hide(); |
| 502 | } |
| 503 | |
| 504 | if ('speakers' in model_settings) { |
| 505 | $('#coqui_api_model_settings').show(); |
| 506 | $('#coqui_api_model_settings_speaker').show(); |
| 507 | $('#coqui_api_model_settings_speaker') |
| 508 | .find('option') |
| 509 | .remove() |
| 510 | .end() |
| 511 | .append('<option value="none">Select speaker</option>') |
| 512 | .val('none'); |
| 513 | |
| 514 | for (let i = 0; i < model_settings.speakers.length; i++) { |
| 515 | const speaker_label = JSON.stringify(model_settings.speakers[i]).replaceAll('"', ''); |
| 516 | $('#coqui_api_model_settings_speaker').append(new Option(speaker_label, i)); |
| 517 | } |
| 518 | } else { |
| 519 | $('#coqui_api_model_settings_speaker').hide(); |
| 520 | } |
| 521 | |
| 522 | $('#coqui_api_model_install_status').text('Requesting model to extras server...'); |
| 523 | $('#coqui_api_model_install_status').show(); |
| 524 | |
| 525 | // Check if already installed and propose to do it otherwise |
| 526 | const model_id = modelDict[model_language][model_dataset][model_name].id; |
| 527 | console.debug(DEBUG_PREFIX, 'Check if model is already installed', model_id); |
| 528 | const result = await CoquiTtsProvider.checkmodel_state(model_id); |
| 529 | const resultJSON = await result.json(); |
| 530 | const model_state = resultJSON.model_state; |
| 531 | |
| 532 | console.debug(DEBUG_PREFIX, ' Model state:', model_state); |
| 533 | |
| 534 | if (model_state == 'installed') { |
| 535 | $('#coqui_api_model_install_status').text('Model already installed on extras server'); |
| 536 | $('#coqui_api_model_install_button').hide(); |
| 537 | } else { |
| 538 | let action = 'download'; |
| 539 | if (model_state == 'corrupted') { |
| 540 | action = 'repare'; |
| 541 | //toastr.error("Click install button to reinstall the model "+$("#coqui_api_model_name").find(":selected").text(), DEBUG_PREFIX+" corrupted model install", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 542 | $('#coqui_api_model_install_status').text('Model found but incomplete try install again (maybe still downloading)'); // (remove and download again) |
| 543 | } else { |
| 544 | toastr.info('Click download button to install the model ' + $('#coqui_api_model_name').find(':selected').text(), DEBUG_PREFIX + ' model not installed', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 545 | $('#coqui_api_model_install_status').text('Model not found on extras server'); |
| 546 | } |
| 547 | |
| 548 | const onModelNameChange_pointer = this.onModelNameChange; |
| 549 | |
| 550 | $('#coqui_api_model_install_button').off('click').on('click', async function () { |
| 551 | try { |
| 552 | $('#coqui_api_model_install_status').text('Downloading model...'); |
| 553 | $('#coqui_api_model_install_button').hide(); |
| 554 | //toastr.info("For model "+model_id, DEBUG_PREFIX+" Started "+action, { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 555 | const apiResult = await CoquiTtsProvider.installModel(model_id, action); |
| 556 | const apiResultJSON = await apiResult.json(); |
| 557 | |
| 558 | console.debug(DEBUG_PREFIX, 'Response:', apiResult); |
| 559 | |
| 560 | if (apiResultJSON.status == 'done') { |
| 561 | $('#coqui_api_model_install_status').text('Model installed and ready to use!'); |
| 562 | $('#coqui_api_model_install_button').hide(); |
| 563 | onModelNameChange_pointer(); |
| 564 | } |
| 565 | |
| 566 | if (apiResultJSON.status == 'downloading') { |
| 567 | toastr.error('Check extras console for progress', DEBUG_PREFIX + ' already downloading', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 568 | $('#coqui_api_model_install_status').text('Already downloading a model, check extras console!'); |
| 569 | $('#coqui_api_model_install_button').show(); |
| 570 | } |
| 571 | } catch (error) { |
| 572 | console.error(error); |
| 573 | toastr.error(error, DEBUG_PREFIX + ' error with model download', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }); |
| 574 | onModelNameChange_pointer(); |
| 575 | } |
| 576 | // will refresh model status |
| 577 | }); |
| 578 | |
| 579 | $('#coqui_api_model_install_button').show(); |
| 580 | return; |
| 581 | } |
| 582 | } |
| 583 | |
| 584 | |
| 585 | //#############################// |
| 586 | // API Calls // |
| 587 | //#############################// |
| 588 | |
| 589 | /* |
| 590 | Check model installation state, return one of ["installed", "corrupted", "absent"] |
| 591 | */ |
| 592 | static async checkmodel_state(model_id) { |
| 593 | throwIfModuleMissing(); |
| 594 | const url = new URL(getApiUrl()); |
| 595 | url.pathname = '/api/text-to-speech/coqui/coqui-api/check-model-state'; |
| 596 | |
| 597 | const apiResult = await doExtrasFetch(url, { |
| 598 | method: 'POST', |
| 599 | headers: { |
| 600 | 'Content-Type': 'application/json', |
| 601 | 'Cache-Control': 'no-cache', |
| 602 | }, |
| 603 | body: JSON.stringify({ |
| 604 | 'model_id': model_id, |
| 605 | }), |
| 606 | }); |
| 607 | |
| 608 | if (!apiResult.ok) { |
| 609 | toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Check model state request failed'); |
| 610 | throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`); |
| 611 | } |
| 612 | |
| 613 | return apiResult; |
| 614 | } |
| 615 | |
| 616 | static async installModel(model_id, action) { |
| 617 | throwIfModuleMissing(); |
| 618 | const url = new URL(getApiUrl()); |
| 619 | url.pathname = '/api/text-to-speech/coqui/coqui-api/install-model'; |
| 620 | |
| 621 | const apiResult = await doExtrasFetch(url, { |
| 622 | method: 'POST', |
| 623 | headers: { |
| 624 | 'Content-Type': 'application/json', |
| 625 | 'Cache-Control': 'no-cache', |
| 626 | }, |
| 627 | body: JSON.stringify({ |
| 628 | 'model_id': model_id, |
| 629 | 'action': action, |
| 630 | }), |
| 631 | }); |
| 632 | |
| 633 | if (!apiResult.ok) { |
| 634 | toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Install model ' + model_id + ' request failed'); |
| 635 | throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`); |
| 636 | } |
| 637 | |
| 638 | return apiResult; |
| 639 | } |
| 640 | |
| 641 | /* |
| 642 | Retrieve user custom models |
| 643 | */ |
| 644 | static async getLocalModelList() { |
| 645 | throwIfModuleMissing(); |
| 646 | const url = new URL(getApiUrl()); |
| 647 | url.pathname = '/api/text-to-speech/coqui/local/get-models'; |
| 648 | |
| 649 | const apiResult = await doExtrasFetch(url, { |
| 650 | method: 'POST', |
| 651 | headers: { |
| 652 | 'Content-Type': 'application/json', |
| 653 | 'Cache-Control': 'no-cache', |
| 654 | }, |
| 655 | body: JSON.stringify({ |
| 656 | 'model_id': 'model_id', |
| 657 | 'action': 'action', |
| 658 | }), |
| 659 | }); |
| 660 | |
| 661 | if (!apiResult.ok) { |
| 662 | toastr.error(apiResult.statusText, DEBUG_PREFIX + ' Get local model list request failed'); |
| 663 | throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`); |
| 664 | } |
| 665 | |
| 666 | return apiResult; |
| 667 | } |
| 668 | |
| 669 | |
| 670 | // Expect voiceId format to be like: |
| 671 | // tts_models/multilingual/multi-dataset/your_tts[2][1] |
| 672 | // tts_models/en/ljspeech/glow-tts |
| 673 | // ts_models/ja/kokoro/tacotron2-DDC |
| 674 | async generateTts(text, voiceId) { |
| 675 | throwIfModuleMissing(); |
| 676 | voiceId = this.settings.customVoices[voiceId]; |
| 677 | |
| 678 | const url = new URL(getApiUrl()); |
| 679 | url.pathname = '/api/text-to-speech/coqui/generate-tts'; |
| 680 | |
| 681 | let language = 'none'; |
| 682 | let speaker = 'none'; |
| 683 | const tokens = voiceId.replaceAll(']', '').replaceAll('"', '').split('['); |
| 684 | const model_id = tokens[0]; |
| 685 | |
| 686 | console.debug(DEBUG_PREFIX, 'Preparing TTS request for', tokens); |
| 687 | |
| 688 | // First option |
| 689 | if (tokens.length > 1) { |
| 690 | const option1 = tokens[1]; |
| 691 | |
| 692 | if (model_id.includes('multilingual')) |
| 693 | language = option1; |
| 694 | else |
| 695 | speaker = option1; |
| 696 | } |
| 697 | |
| 698 | // Second option |
| 699 | if (tokens.length > 2) |
| 700 | speaker = tokens[2]; |
| 701 | |
| 702 | const apiResult = await doExtrasFetch(url, { |
| 703 | method: 'POST', |
| 704 | headers: { |
| 705 | 'Content-Type': 'application/json', |
| 706 | 'Cache-Control': 'no-cache', |
| 707 | }, |
| 708 | body: JSON.stringify({ |
| 709 | 'text': text, |
| 710 | 'model_id': model_id, |
| 711 | 'language_id': parseInt(language), |
| 712 | 'speaker_id': parseInt(speaker), |
| 713 | }), |
| 714 | }); |
| 715 | |
| 716 | if (!apiResult.ok) { |
| 717 | toastr.error(apiResult.statusText, 'TTS Generation Failed'); |
| 718 | throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`); |
| 719 | } |
| 720 | |
| 721 | return apiResult; |
| 722 | } |
| 723 | |
| 724 | // Dirty hack to say not implemented |
| 725 | async fetchTtsVoiceObjects() { |
| 726 | const voiceIds = Object |
| 727 | .keys(this.settings.voiceMapDict) |
| 728 | .map(voice => ({ name: voice, voice_id: voice, preview_url: false })); |
| 729 | return voiceIds; |
| 730 | } |
| 731 | |
| 732 | // Do nothing |
| 733 | previewTtsVoice(id) { |
| 734 | return; |
| 735 | } |
| 736 | |
| 737 | async fetchTtsFromHistory(history_item_id) { |
| 738 | return Promise.resolve(history_item_id); |
| 739 | } |
| 740 | } |
| 741 | |
| 742 | async function initLocalModels() { |
| 743 | if (!modules.includes('coqui-tts')) |
| 744 | return; |
| 745 | |
| 746 | // Initialized local model once |
| 747 | if (!coquiLocalModelsReceived) { |
| 748 | const result = await CoquiTtsProvider.getLocalModelList(); |
| 749 | const resultJSON = await result.json(); |
| 750 | |
| 751 | coquiLocalModels = resultJSON.models_list; |
| 752 | |
| 753 | $('#coqui_local_model_name').show(); |
| 754 | $('#coqui_local_model_name') |
| 755 | .find('option') |
| 756 | .remove() |
| 757 | .end() |
| 758 | .append('<option value="none">Select model</option>') |
| 759 | .val('none'); |
| 760 | |
| 761 | for (const model_dataset of coquiLocalModels) |
| 762 | $('#coqui_local_model_name').append(new Option(model_dataset, model_dataset)); |
| 763 | |
| 764 | coquiLocalModelsReceived = true; |
| 765 | } |
| 766 | } |