Blame Raw
Cohee · e3f41666 · · 766 lines (29.1 KB)
1 contributor
1/*
2TODO:
3 - Hide voice map its just confusing
4 - Delete useless call
5*/
6
7import { doExtrasFetch, extension_settings, getApiUrl, modules } from '../../extensions.js';
8import { initVoiceMap } from './index.js';
9import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
10
11export { CoquiTtsProvider };
12
13const DEBUG_PREFIX = '<Coqui TTS module> ';
14
15let inApiCall = false;
16let coquiApiModels = {}; // Initialized only once
17let coquiApiModelsFull = {}; // Initialized only once
18let coquiLocalModels = []; // Initialized only once
19let coquiLocalModelsReceived = false;
20/*
21coquiApiModels 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*/
35const languageLabels = {
36 'multilingual': 'Multilingual',
37 'en': 'English',
38 'fr': 'French',
39 'es': 'Spanish',
40 'ja': 'Japanese',
41};
42
43function 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
51function resetModelSettings() {
52 $('#coqui_api_model_settings_language').val('none');
53 $('#coqui_api_model_settings_speaker').val('none');
54}
55
56class 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
742async 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}