Finished unefficient full coqui pipeline, UI generated through request to extras. Need to be pruned
This commit is contained in:
parent
1eb4dd9a4f
commit
e60f91ce42
|
@ -6,30 +6,66 @@ TODO:
|
|||
*/
|
||||
|
||||
import { eventSource, event_types } from "../../../script.js"
|
||||
import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js"
|
||||
import { doExtrasFetch, extension_settings, getApiUrl, getContext, modules, ModuleWorkerWrapper } from "../../extensions.js"
|
||||
|
||||
export { CoquiTtsProvider }
|
||||
|
||||
const DEBUG_PREFIX = "<Coqui module> "
|
||||
const DEBUG_PREFIX = "<Coqui TTS module> "
|
||||
const UPDATE_INTERVAL = 1000
|
||||
|
||||
let inApiCall = false
|
||||
let charactersList = [] // Updated with module worker
|
||||
let coquiApiModels = {} // Initialized only once
|
||||
/*
|
||||
coquiApiModels format [language][dataset][name]:coqui-api-model-id, example:
|
||||
{
|
||||
"en": {
|
||||
"vctk": {
|
||||
"vits": "tts_models/en/vctk/vits"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"kokoro": {
|
||||
"tacotron2-DDC": "tts_models/ja/kokoro/tacotron2-DDC"
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
const languageLabels = {
|
||||
"multilingual": "Multilingual",
|
||||
"en" : "English",
|
||||
"fr" : "French",
|
||||
"es" : "Spanish",
|
||||
"ja" : "Japanese"
|
||||
}
|
||||
|
||||
function throwIfModuleMissing() {
|
||||
if (!modules.includes('coqui-tts')) {
|
||||
toastr.error(`Coqui TTS module not loaded. Add coqui-tts to enable-modules and restart the Extras API.`)
|
||||
throw new Error(`Coqui TTS module not loaded.`)
|
||||
toastr.error(`Add coqui-tts to enable-modules and restart the Extras API.`, "Coqui TTS module not loaded.", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
throw new Error(DEBUG_PREFIX,`Coqui TTS module not loaded.`);
|
||||
}
|
||||
}
|
||||
|
||||
function throwLocalOrigin() {
|
||||
toastr.info("coming soon, ready when ready, etc", DEBUG_PREFIX+' Custom models not supported yet', { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
throw new Error(DEBUG_PREFIX,`requesting feature not implemented yet.`);
|
||||
}
|
||||
|
||||
function resetModelSettings() {
|
||||
$("#coqui_api_model_settings_language").val("none");
|
||||
$("#coqui_api_model_settings_speaker").val("none");
|
||||
}
|
||||
|
||||
class CoquiTtsProvider {
|
||||
//#############################//
|
||||
// Extension UI and Settings //
|
||||
//#############################//
|
||||
|
||||
settings
|
||||
voices = []
|
||||
separator = ' .. '
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {}
|
||||
voiceMap: "",
|
||||
voiceMapDict : {}
|
||||
}
|
||||
|
||||
get settingsHtml() {
|
||||
|
@ -39,28 +75,36 @@ class CoquiTtsProvider {
|
|||
<div style="flex: 50%;">
|
||||
<label for="coqui_character_select">Character:</label>
|
||||
<select id="coqui_character_select">
|
||||
<option value="none">Select Character</option>
|
||||
<!-- Populated by JS -->
|
||||
</select>
|
||||
<label for="coqui_model_origin">Models:</label>
|
||||
<select id="coqui_model_origin">
|
||||
<option value="none">Select Origin</option>
|
||||
<option value="coquiApi">Coqui TTS</option>
|
||||
<option value="coqui-api">Coqui TTS</option>
|
||||
<option value="local">My models</option>
|
||||
</select>
|
||||
|
||||
<div id="coqui_api_model_div">
|
||||
<select id="coqui_api_language">
|
||||
<option value="none">Select model language</option>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<!-- Populated by JS and request -->
|
||||
</select>
|
||||
|
||||
<select id="coqui_api_model_name">
|
||||
<!-- Populated by JS and request -->
|
||||
</select>
|
||||
|
||||
<input id="coqui_api_model_install_button" class="menu_button" type="submit" value="Install" />
|
||||
|
||||
<span id="coqui_api_model_loading">Loading...</span>
|
||||
|
||||
<div id="coqui_api_model_settings">
|
||||
<select id="coqui_api_model_settings_language">
|
||||
<!-- Populated by JS and request -->
|
||||
</select>
|
||||
<select id="coqui_api_model_settings_speaker">
|
||||
<!-- Populated by JS and request -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -70,17 +114,95 @@ class CoquiTtsProvider {
|
|||
}
|
||||
|
||||
loadSettings(settings) {
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings
|
||||
|
||||
for (const key in settings){
|
||||
if (key in this.settings){
|
||||
this.settings[key] = settings[key]
|
||||
} else {
|
||||
throw DEBUG_PREFIX+`Invalid setting passed to extension: ${key}`
|
||||
}
|
||||
}
|
||||
|
||||
$("#coqui_api_model_div").hide();
|
||||
$("#coqui_api_model_name").hide();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
$("#coqui_api_model_loading").hide();
|
||||
$("#coqui_api_model_install_button").hide();
|
||||
$("#coqui_model_origin").on("change",this.onModelOriginChange);
|
||||
$("#coqui_api_language").on("change",this.onModelLanguageChange);
|
||||
|
||||
$("#coqui_api_model_name").on("change",this.onModelNameChange);
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
}
|
||||
|
||||
async onApplyClick() {
|
||||
if (inApiCall) {
|
||||
return; // TOdo block dropdown
|
||||
}
|
||||
|
||||
const character = $("#coqui_character_select").val();
|
||||
const model_origin = $("#coqui_model_origin").val();
|
||||
const model_language = $("#coqui_api_language").val();
|
||||
const model_name = $("#coqui_api_model_name").val();
|
||||
let model_setting_language = $("#coqui_api_model_settings_language").val();
|
||||
let model_setting_speaker = $("#coqui_api_model_settings_speaker").val();
|
||||
|
||||
|
||||
if (character === "none") {
|
||||
toastr.error(`Character not selected, please select one.`, DEBUG_PREFIX+" voice mapping character", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_origin == "none") {
|
||||
toastr.error(`Origin not selected, please select one.`, DEBUG_PREFIX+" voice mapping origin", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_origin == "local") {
|
||||
throwLocalOrigin();
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_language == "none") {
|
||||
toastr.error(`Language not selected, please select one.`, DEBUG_PREFIX+" voice mapping language", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_name == "none") {
|
||||
toastr.error(`Model not selected, please select one.`, DEBUG_PREFIX+" voice mapping model", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (model_setting_language == "none")
|
||||
model_setting_language = null;
|
||||
|
||||
if (model_setting_speaker == "none")
|
||||
model_setting_speaker = null;
|
||||
|
||||
console.debug(DEBUG_PREFIX,"Current voice map: ",this.settings.voiceMap);
|
||||
|
||||
this.settings.voiceMapDict[character] = {model_id: model_name, model_language:model_setting_language, model_speaker:model_setting_speaker};
|
||||
|
||||
console.debug(DEBUG_PREFIX,"Registered new voice map: ",character,":",this.settings.voiceMapDict[character]);
|
||||
|
||||
this.settings.voiceMap = "";
|
||||
for(let i in this.settings.voiceMapDict) {
|
||||
const voice_settings = this.settings.voiceMapDict[i];
|
||||
this.settings.voiceMap += i + ":" + voice_settings["model_id"];
|
||||
|
||||
if (model_setting_language != null)
|
||||
this.settings.voiceMap += "[" + voice_settings["model_language"] + "]";
|
||||
|
||||
if (model_setting_speaker != null)
|
||||
this.settings.voiceMap += "[" + voice_settings["model_speaker"] + "]";
|
||||
|
||||
this.settings.voiceMap += ",";
|
||||
}
|
||||
$("#tts_voice_map").val(this.settings.voiceMap);
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -93,11 +215,13 @@ class CoquiTtsProvider {
|
|||
}
|
||||
|
||||
async onModelOriginChange() {
|
||||
throwIfModuleMissing()
|
||||
resetModelSettings();
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
console.debug(model_origin);
|
||||
|
||||
// TODO: show coqui model list
|
||||
if (model_origin == "coquiApi") {
|
||||
if (model_origin == "coqui-api") {
|
||||
$("#coqui_api_model_div").show();
|
||||
}
|
||||
else
|
||||
|
@ -105,12 +229,15 @@ class CoquiTtsProvider {
|
|||
|
||||
// TODO show local model list
|
||||
if (model_origin == "local") {
|
||||
toastr.info("coming soon, ready when ready, etc", DEBUG_PREFIX+' Custom models not supported yet');
|
||||
throwLocalOrigin();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async onModelLanguageChange() {
|
||||
throwIfModuleMissing();
|
||||
resetModelSettings();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
const model_origin = $('#coqui_model_origin').val();
|
||||
const model_language = $('#coqui_api_language').val();
|
||||
console.debug(model_language);
|
||||
|
||||
|
@ -120,58 +247,254 @@ class CoquiTtsProvider {
|
|||
}
|
||||
|
||||
$("#coqui_api_model_name").show();
|
||||
|
||||
// TODO: if model list not already load, request it from extras
|
||||
const result = await CoquiTtsProvider.getCoquiApiModels();
|
||||
const models = await result.json();
|
||||
console.debug(models,typeof(models));
|
||||
console.debug("models lists:", models[model_language]);
|
||||
|
||||
$('#coqui_api_model_name')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select model</option>')
|
||||
.val('whatever')
|
||||
.val('none');
|
||||
|
||||
for(let k in models[model_language]) {
|
||||
let model_name = k.split("/")
|
||||
model_name = model_name[0] + " ("+model_name[1]+" dataset)"
|
||||
$("#coqui_api_model_name").append(new Option(model_name,models[model_language][k]));
|
||||
for(let model_dataset in coquiApiModels[model_language])
|
||||
for(let model_name in coquiApiModels[model_language][model_dataset]) {
|
||||
const model_id = coquiApiModels[model_language][model_dataset][model_name]
|
||||
const model_label = model_name + " ("+model_dataset+" dataset)"
|
||||
$("#coqui_api_model_name").append(new Option(model_label,model_id));
|
||||
}
|
||||
|
||||
// TODO: populate with corresponding dataset/model pairs got from initialization request
|
||||
|
||||
}
|
||||
|
||||
async onModelNameChange() {
|
||||
throwIfModuleMissing();
|
||||
resetModelSettings();
|
||||
$("#coqui_api_model_settings").hide();
|
||||
|
||||
const modelId = $("#coqui_api_model_name").val();
|
||||
|
||||
if (modelId == "none") {
|
||||
$("#coqui_api_model_install_button").off('click');
|
||||
$("#coqui_api_model_install_button").show();
|
||||
return;
|
||||
}
|
||||
|
||||
$("#coqui_api_model_loading").show();
|
||||
|
||||
// Check if already download and propose to do it
|
||||
console.debug(DEBUG_PREFIX,"Check if model is already installed",modelId);
|
||||
let result = await CoquiTtsProvider.checkModelState(modelId);
|
||||
result = await result.json();
|
||||
const modelState = result["model_state"];
|
||||
|
||||
console.debug(DEBUG_PREFIX," Model state:", modelState)
|
||||
|
||||
if (modelState != "installed") {
|
||||
$("#coqui_api_model_install_button").show();
|
||||
$("#coqui_api_model_install_button").on("click", function (){
|
||||
CoquiTtsProvider.installModel(modelId);
|
||||
});
|
||||
|
||||
if (modelState == "corrupted") {
|
||||
toastr.error("Click repare button to reinstall the model "+$("#coqui_api_model_name").find(":selected").text(), DEBUG_PREFIX+" corrupted model install", { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true });
|
||||
$("#coqui_api_model_install_button").val("Repare");
|
||||
}
|
||||
else {
|
||||
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 });
|
||||
$("#coqui_api_model_install_button").val("Download");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX,"Request model settings for",modelId);
|
||||
|
||||
result = await CoquiTtsProvider.getModelSettings(modelId);
|
||||
const modelSettings = await result.json();
|
||||
$("#coqui_api_model_loading").hide();
|
||||
|
||||
console.debug(DEBUG_PREFIX,modelSettings)
|
||||
|
||||
|
||||
if (modelSettings["languages"].length > 0) {
|
||||
$("#coqui_api_model_settings").show();
|
||||
$("#coqui_api_model_settings_language").show();
|
||||
$('#coqui_api_model_settings_language')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select language</option>')
|
||||
.val('none');
|
||||
|
||||
for(const language of modelSettings["languages"]) {
|
||||
$("#coqui_api_model_settings_language").append(new Option(language,language));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$("#coqui_api_model_settings_language").hide();
|
||||
}
|
||||
|
||||
if (modelSettings["speakers"].length > 0) {
|
||||
$("#coqui_api_model_settings").show();
|
||||
$("#coqui_api_model_settings_speaker").show();
|
||||
$('#coqui_api_model_settings_speaker')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select speaker</option>')
|
||||
.val('none');
|
||||
|
||||
for(const speaker of modelSettings["speakers"]) {
|
||||
$("#coqui_api_model_settings_speaker").append(new Option(speaker,speaker));
|
||||
}
|
||||
}
|
||||
else {
|
||||
$("#coqui_api_model_settings_speaker").hide();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//#############################//
|
||||
// API Calls //
|
||||
//#############################//
|
||||
|
||||
static async getCoquiApiModels() {
|
||||
static async getModelList(origin) {
|
||||
throwIfModuleMissing()
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/supported-models';
|
||||
|
||||
if (origin == "coqui-api")
|
||||
url.pathname = '/api/text-to-speech/coqui/coqui-api/get-models';
|
||||
else
|
||||
url.pathname = '/api/text-to-speech/coqui/local/get-models';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {method: 'POST'})
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX+' Get models list failed');
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX+' Get models list request failed');
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
}
|
||||
|
||||
/*
|
||||
Check model installation state, return one of ["installed", "corrupted", "absent"]
|
||||
*/
|
||||
static async checkModelState(modelId) {
|
||||
throwIfModuleMissing()
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/coqui-api/check-model-state';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model_id": modelId,
|
||||
})
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX+' Check model state request failed');
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
}
|
||||
|
||||
static async installModel(modelId) {
|
||||
throwIfModuleMissing()
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/coqui-api/install-model';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model_id": modelId,
|
||||
})
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX+' Install model '+modelId+' request failed');
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
}
|
||||
|
||||
static async getModelSettings(modelId) {
|
||||
throwIfModuleMissing()
|
||||
// API is busy
|
||||
inApiCall = true;
|
||||
|
||||
try {
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/coqui-api/get-model-settings';
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-cache'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"model_id": modelId,
|
||||
})
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, DEBUG_PREFIX+' Get model setting request failed for '+modelId);
|
||||
throw new Error(`HTTP ${apiResult.status}: ${await apiResult.text()}`);
|
||||
}
|
||||
|
||||
return apiResult
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error);
|
||||
}
|
||||
finally {
|
||||
inApiCall = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get speakers
|
||||
|
||||
|
||||
// Expect voiceId format to be like:
|
||||
// tts_models/multilingual/multi-dataset/your_tts[2][1]
|
||||
// tts_models/en/ljspeech/glow-tts
|
||||
// ts_models/ja/kokoro/tacotron2-DDC
|
||||
async generateTts(text, voiceId) {
|
||||
throwIfModuleMissing()
|
||||
const url = new URL(getApiUrl());
|
||||
url.pathname = '/api/text-to-speech/coqui/process-text';
|
||||
|
||||
let language = "none"
|
||||
let speaker = "none"
|
||||
const tokens = voiceId.replaceAll("]","").split("[");
|
||||
const modelId = tokens[0]
|
||||
|
||||
console.debug(DEBUG_PREFIX,"Preparing TTS request for",tokens)
|
||||
|
||||
// First option
|
||||
if (tokens.length > 1) {
|
||||
const option1 = tokens[1]
|
||||
|
||||
if (modelId.includes("multilingual"))
|
||||
language = option1
|
||||
else
|
||||
speaker = option1
|
||||
}
|
||||
|
||||
// Second option
|
||||
if (tokens.length > 2)
|
||||
speaker = tokens[2];
|
||||
|
||||
const apiResult = await doExtrasFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -180,9 +503,11 @@ class CoquiTtsProvider {
|
|||
},
|
||||
body: JSON.stringify({
|
||||
"text": text,
|
||||
"voiceId": voiceId,
|
||||
"model_id": modelId,
|
||||
"language": language,
|
||||
"speaker": speaker
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if (!apiResult.ok) {
|
||||
toastr.error(apiResult.statusText, 'TTS Generation Failed');
|
||||
|
@ -198,3 +523,65 @@ class CoquiTtsProvider {
|
|||
return Promise.resolve(history_item_id);
|
||||
}
|
||||
}
|
||||
|
||||
//#############################//
|
||||
// Module Worker //
|
||||
//#############################//
|
||||
|
||||
async function moduleWorker() {
|
||||
// Skip if module not loaded
|
||||
if (!modules.includes('coqui-tts'))
|
||||
return;
|
||||
|
||||
// 1) Update characters list
|
||||
//----------------------------
|
||||
let currentcharacters = new Set();
|
||||
for (const i of getContext().characters) {
|
||||
currentcharacters.add(i.name);
|
||||
}
|
||||
|
||||
currentcharacters = Array.from(currentcharacters)
|
||||
|
||||
if (JSON.stringify(charactersList) !== JSON.stringify(currentcharacters)) {
|
||||
charactersList = currentcharacters
|
||||
|
||||
$('#coqui_character_select')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select Character</option>')
|
||||
.val('none')
|
||||
|
||||
for(const charName of charactersList) {
|
||||
$("#coqui_character_select").append(new Option(charName,charName));
|
||||
}
|
||||
|
||||
console.debug(DEBUG_PREFIX,"Updated character list to:", charactersList);
|
||||
}
|
||||
|
||||
// 2) Initialize TTS models lists
|
||||
//---------------------------------
|
||||
if (Object.keys(coquiApiModels).length === 0) {
|
||||
const result = await CoquiTtsProvider.getModelList("coqui-api");
|
||||
coquiApiModels = await result.json();
|
||||
console.debug(DEBUG_PREFIX,"initialized coqui-api model list to", coquiApiModels);
|
||||
|
||||
$('#coqui_api_language')
|
||||
.find('option')
|
||||
.remove()
|
||||
.end()
|
||||
.append('<option value="none">Select model language</option>')
|
||||
.val('none');
|
||||
|
||||
for(let language in coquiApiModels) {
|
||||
$("#coqui_api_language").append(new Option(languageLabels[language],language));
|
||||
console.log(DEBUG_PREFIX,"added language",language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
|
||||
moduleWorker();
|
||||
})
|
Loading…
Reference in New Issue