"use strict" const er = require('@electron/remote') const dialog = er.dialog window.voiceWorkbenchState = { isInit: false, isStarted: false, currentAudioFilePath: undefined, newAudioFilePath: undefined, currentEmb: undefined, refAEmb: undefined, refBEmb: undefined, } window.initVoiceWorkbench = () => { if (!window.voiceWorkbenchState.isInit) { window.voiceWorkbenchState.isInit = true window.refreshExistingCraftedVoices() window.initDropdowns() voiceWorkbenchLanguageDropdown.value = "en" } window.refreshExistingCraftedVoices() } window.refreshExistingCraftedVoices = () => { voiceWorkbenchVoicesList.innerHTML = "" Object.keys(window.games).sort((a,b)=>a>b?1:-1).forEach(gameId => { if (Object.keys(window.gameAssets).includes(gameId)) { const themeColour = window.gameAssets[gameId].themeColourPrimary window.games[gameId].models.forEach(model => { if (model.embOverABaseModel) { const button = createElem("div.voiceType", model.voiceName) button.style.background = `#${themeColour}` button.addEventListener("click", () => window.voiceWorkbenchLoadOrResetCraftedVoice(model)) voiceWorkbenchVoicesList.appendChild(button) } }) } }) } window.voiceWorkbenchLoadOrResetCraftedVoice = (model) => { voiceWorkbenchModelDropdown.value = model ? model.embOverABaseModel : "/base_v1.0" voiceWorkbenchVoiceNameInput.value = model ? model.voiceName : "" voiceWorkbenchVoiceIDInput.value = model ? model.variants[0].voiceId : "" voiceWorkbenchGenderDropdown.value = model ? model.variants[0].gender : "male" voiceWorkbenchAuthorInput.value = model ? (model.variants[0].author || "Anonymous") : "" voiceWorkbenchLanguageDropdown.value = model ? model.variants[0].lang : "en" voiceWorkbenchGamesDropdown.value = model ? model.gameId : "other" voiceWorkbenchCurrentEmbeddingInput.value = model ? model.variants[0].base_speaker_emb : "" window.voiceWorkbenchState.currentEmb = model ? model.variants[0].base_speaker_emb : undefined window.voiceWorkbenchState.currentAudioFilePath = undefined window.voiceWorkbenchState.newAudioFilePath = undefined window.voiceWorkbenchState.refAEmb = undefined window.voiceWorkbenchState.refBEmb = undefined voiceWorkbenchRefAInput.value = "" voiceWorkbenchRefBInput.value = "" if (model) { voiceWorkbenchDeleteButton.disabled = false voiceWorkbenchStartButton.click() } } window.voiceWorkbenchGenerateVoice = async () => { if (!voiceWorkbenchCurrentEmbeddingInput.value.length) { return window.errorModal(window.i18n.ENTER_VOICE_CRAFTING_STARTING_EMB) } // Load the model if it hasn't been loaded already let voiceId = voiceWorkbenchModelDropdown.value.split("/").at(-1) if (!window.currentModel || window.currentModel.voiceId!=voiceId) { let modelPath if (voiceId.includes("Base xVAPitch Model")) { modelPath = `${window.path}/python/xvapitch/base_v1.0.pt` } else { const gameId = voiceWorkbenchModelDropdown.value.split("/").at(0) modelPath = window.userSettings[`modelspath_${gameId}`]+"/"+voiceId } await window.voiceWorkbenchChangeModel(modelPath, voiceId) } const base_lang = voiceWorkbenchLanguageDropdown.value//voiceId.includes("Base xVAPitch Model") ? "en" : window.currentModel.lang let newEmb = undefined // const currentEmbedding = voiceWorkbenchCurrentEmbeddingInput.value.split(",").map(v=>parseFloat(v)) const currentEmbedding = window.voiceWorkbenchState.currentEmb // const currentDelta = voiceWorkbenchCurrentDeltaInput.value.split(",").map(v=>parseFloat(v)) const newEmbedding = window.getVoiceWorkbenchNewEmbedding() const tempFileNum = `${Math.random().toString().split(".")[1]}` const currentTempFileLocation = `${path}/output/temp-${tempFileNum}_current.wav` const newTempFileLocation = `${path}/output/temp-${tempFileNum}_new.wav` // Do the current embedding first const synthRequests = [] synthRequests.push(doSynth(JSON.stringify({ sequence: voiceWorkbenchInputTextArea.value.trim(), useCleanup: true, // TODO, user setting? base_lang, base_emb: currentEmbedding.join(","), outfile: currentTempFileLocation }))) let doingNewAudioFile = false if (voiceWorkbenchCurrentDeltaInput.value.length) { doingNewAudioFile = true synthRequests.push(doSynth(JSON.stringify({ sequence: voiceWorkbenchInputTextArea.value.trim(), useCleanup: true, // TODO, user setting? base_lang, base_emb: newEmbedding.join(","), outfile: newTempFileLocation }))) } // toggleSpinnerButtons() spinnerModal(`${window.i18n.SYNTHESIZING}`) Promise.all(synthRequests).then(res => { closeModal(undefined, [workbenchContainer]) window.voiceWorkbenchState.currentAudioFilePath = currentTempFileLocation voiceWorkbenchAudioCurrentPlayPauseBtn.disabled = false voiceWorkbenchAudioCurrentSaveBtn.disabled = false if (doingNewAudioFile) { window.voiceWorkbenchState.newAudioFilePath = newTempFileLocation voiceWorkbenchAudioNewPlayBtn.disabled = false voiceWorkbenchAudioNewSaveBtn.disabled = false } }) } const doSynth = (body) => { return new Promise(resolve => { doFetch("http://localhost:8008/synthesizeSimple", { method: "Post", body }).then(r=>r.text()).then(resolve) }) } window.getVoiceWorkbenchNewEmbedding = () => { const currentDelta = voiceWorkbenchCurrentDeltaInput.value.split(",").map(v=>parseFloat(v)) const newEmb = window.voiceWorkbenchState.currentEmb.map((v,vi) => { return v + currentDelta[vi]//*strength }) return newEmb } window.voiceWorkbenchChangeModel = (modelPath, voiceId) => { window.currentModel = { outputs: undefined, model: modelPath.replace(".pt", ""), modelType: "xVAPitch", base_lang: voiceWorkbenchLanguageDropdown.value, isBaseModel: true, voiceId: voiceId } generateVoiceButton.dataset.modelQuery = JSON.stringify(window.currentModel) return window.loadModel() } voiceWorkbenchGenerateSampleButton.addEventListener("click", window.voiceWorkbenchGenerateVoice) window.initDropdowns = () => { // Games dropdown Object.keys(window.games).sort((a,b)=>a>b?1:-1).forEach(gameId => { if (gameId!="other") { if (Object.keys(window.gameAssets).includes(gameId)) { const gameName = window.games[gameId].gameTheme.gameName const option = createElem("option", gameName) option.value = gameId voiceWorkbenchGamesDropdown.appendChild(option) } } }) // Models dropdown Object.keys(window.games).forEach(gameId => { if (window.games[gameId].gameTheme) { const gameName = window.games[gameId].gameTheme.gameName window.games[gameId].models.forEach(modelMeta => { const voiceName = modelMeta.voiceName const voiceId = modelMeta.variants[0].voiceId // Variants are not supported by v3 models, so pick the first one only. Also, filter out crafted voices if (modelMeta.variants[0].modelType=="xVAPitch" && !modelMeta.embOverABaseModel) { const option = createElem("option", `[${gameName}] ${voiceName}`) option.value = `${gameId}/${voiceId}` voiceWorkbenchModelDropdown.appendChild(option) } }) } }) } // Change the available languages when the model is changed voiceWorkbenchModelDropdown.addEventListener("change", () => { let voiceId = voiceWorkbenchModelDropdown.value.split("/").at(-1) if (voiceId.includes("base_v1.0")) { window.populateLanguagesDropdownsFromModel(voiceWorkbenchLanguageDropdown) voiceWorkbenchLanguageDropdown.value = "en" } else { const gameId = voiceWorkbenchModelDropdown.value.split("/")[0] if (Object.keys(window.games).includes(gameId)) { const baseModelData = window.games[gameId].models.filter(model => { return model.variants[0].voiceId == voiceWorkbenchModelDropdown.value.split("/").at(-1) })[0] window.populateLanguagesDropdownsFromModel(voiceWorkbenchLanguageDropdown, baseModelData) voiceWorkbenchLanguageDropdown.value = baseModelData.variants[0].lang } } }) voiceWorkbenchStartButton.addEventListener("click", () => { window.voiceWorkbenchState.isStarted = true voiceWorkbenchLoadedContent.style.display = "flex" voiceWorkbenchLoadedContent2.style.display = "flex" voiceWorkbenchStartButton.style.display = "none" // Load the base model's embedding as a starting point, if it's not the built-in base model let voiceId = voiceWorkbenchModelDropdown.value.split("/").at(-1) if (voiceId.includes("base_v1.0")) { } else { const gameId = voiceWorkbenchModelDropdown.value.split("/")[0] if (Object.keys(window.games).includes(gameId)) { const baseModelData = window.games[gameId].models.filter(model => { return model.variants[0].voiceId == voiceWorkbenchModelDropdown.value.split("/").at(-1) })[0] voiceWorkbenchCurrentEmbeddingInput.value = baseModelData.variants[0].base_speaker_emb.join(",") window.voiceWorkbenchState.currentEmb = baseModelData.variants[0].base_speaker_emb } } }) window.setupVoiceWorkbenchDropArea = (container, inputField, callback=undefined) => { const dropFn = (eType, event) => { if (["dragenter", "dragover"].includes(eType)) { container.style.background = "#5b5b5b" container.style.color = "white" } if (["dragleave", "drop"].includes(eType)) { container.style.background = "rgba(0,0,0,0)" container.style.color = "white" } event.preventDefault() event.stopPropagation() const dataLines = [] if (eType=="drop") { const dataTransfer = event.dataTransfer const files = Array.from(dataTransfer.files) if (files[0].path.endsWith(".wav")) { const filePath = String(files[0].path).replaceAll(/\\/g, "/") console.log("filePath", filePath) window.getSpeakerEmbeddingFromFilePath(filePath).then(embedding => { inputField.value = embedding if (callback) { callback(filePath) } }) } else { window.errorModal(window.i18n.ERROR_FILE_MUST_BE_WAV) } } } container.addEventListener("dragenter", event => dropFn("dragenter", event), false) container.addEventListener("dragleave", event => dropFn("dragleave", event), false) container.addEventListener("dragover", event => dropFn("dragover", event), false) container.addEventListener("drop", event => dropFn("drop", event), false) } window.setupVoiceWorkbenchDropArea(voiceWorkbenchCurrentEmbeddingDropzone, voiceWorkbenchCurrentEmbeddingInput, () => { window.voiceWorkbenchState.currentEmb = voiceWorkbenchCurrentEmbeddingInput.value.split(",").map(v=>parseFloat(v)) }) voiceWorkbenchCurrentEmbeddingInput.addEventListener("change", ()=>{ window.voiceWorkbenchState.currentEmb = voiceWorkbenchCurrentEmbeddingInput.value.split(",").map(v=>parseFloat(v)) }) window.setupVoiceWorkbenchDropArea(voiceWorkbenchRefADropzone, voiceWorkbenchRefAInput, (filePath) => { voiceWorkbenchRefAFilePath.innerHTML = window.i18n.FROM_FILE_IS_FILEPATH.replace("_1", filePath) voiceWorkshopApplyDeltaButton.disabled = false window.voiceWorkbenchState.refAEmb = voiceWorkbenchRefAInput.value.split(",").map(v=>parseFloat(v)) window.voiceWorkbenchUpdateDelta() }) window.setupVoiceWorkbenchDropArea(voiceWorkbenchRefBDropzone, voiceWorkbenchRefBInput, (filePath) => { voiceWorkbenchRefBFilePath.innerHTML = window.i18n.FROM_FILE_IS_FILEPATH.replace("_1", filePath) window.voiceWorkbenchState.refBEmb = voiceWorkbenchRefBInput.value.split(",").map(v=>parseFloat(v)) window.voiceWorkbenchUpdateDelta() }) voiceWorkbenchInputTextArea.addEventListener("keyup", () => { voiceWorkbenchGenerateSampleButton.disabled = voiceWorkbenchInputTextArea.value.trim().length==0 }) voiceWorkbenchAudioCurrentPlayPauseBtn.addEventListener("click", () => { const audioPreview = createElem("audio", {autoplay: false}, createElem("source", { src: window.voiceWorkbenchState.currentAudioFilePath })) audioPreview.setSinkId(window.userSettings.base_speaker) }) voiceWorkbenchAudioCurrentSaveBtn.addEventListener("click", async () => { const userChosenPath = await dialog.showSaveDialog({ defaultPath: window.voiceWorkbenchState.currentAudioFilePath }) if (userChosenPath && userChosenPath.filePath) { const outFilePath = userChosenPath.filePath.split(".").at(-1)=="wav" ? userChosenPath.filePath : userChosenPath.filePath+".wav" fs.copyFileSync(window.voiceWorkbenchState.currentAudioFilePath, outFilePath) } }) voiceWorkbenchAudioNewPlayBtn.addEventListener("click", () => { const audioPreview = createElem("audio", {autoplay: false}, createElem("source", { src: window.voiceWorkbenchState.newAudioFilePath })) audioPreview.setSinkId(window.userSettings.base_speaker) }) voiceWorkbenchAudioNewSaveBtn.addEventListener("click", async () => { const userChosenPath = await dialog.showSaveDialog({ defaultPath: window.voiceWorkbenchState.newAudioFilePath }) if (userChosenPath && userChosenPath.filePath) { const outFilePath = userChosenPath.filePath.split(".").at(-1)=="wav" ? userChosenPath.filePath : userChosenPath.filePath+".wav" fs.copyFileSync(window.voiceWorkbenchState.newAudioFilePath, outFilePath) } }) window.voiceWorkbenchUpdateDelta = () => { // Don't do anything if reference file A isn't given if (!window.voiceWorkbenchState.refAEmb) { return } const strengthValue = parseFloat(voiceWorkbenchStrengthInput.value) let delta // When only Ref A is used, the delta is from towards the first reference file A if (window.voiceWorkbenchState.refBEmb == undefined) { delta = window.voiceWorkbenchState.currentEmb.map((v,vi) => { return (window.voiceWorkbenchState.refAEmb[vi] - v) * strengthValue }) } else { // When Ref B is also used, the delta is from ref A to ref B delta = window.voiceWorkbenchState.refAEmb.map((v,vi) => { return (window.voiceWorkbenchState.refBEmb[vi] - v) * strengthValue }) } voiceWorkbenchCurrentDeltaInput.value = delta.join(",") } voiceWorkbenchStrengthSlider.addEventListener("change", () => { voiceWorkbenchStrengthInput.value = voiceWorkbenchStrengthSlider.value window.voiceWorkbenchUpdateDelta() }) voiceWorkbenchStrengthInput.addEventListener("change", () => { voiceWorkbenchStrengthSlider.value = voiceWorkbenchStrengthInput.value window.voiceWorkbenchUpdateDelta() }) voiceWorkshopApplyDeltaButton.addEventListener("click", () => { if (voiceWorkbenchCurrentDeltaInput.value.length) { const newEmb = window.getVoiceWorkbenchNewEmbedding() window.voiceWorkbenchState.currentEmb = newEmb voiceWorkbenchCurrentEmbeddingInput.value = newEmb.join(",") voiceWorkbenchCurrentDeltaInput.value = "" voiceWorkshopApplyDeltaButton.disabled = true voiceWorkbenchRefAInput.value = "" window.voiceWorkbenchState.refAEmb = undefined voiceWorkbenchRefBInput.value = "" window.voiceWorkbenchState.refBEmb = undefined } }) /* Drop file A over the reference audio file A area, to get its embedding When only the reference A file is used, the current delta is this embedding multiplied by the strength Drop file B over the B area, to get a second embedding When both this and A are active, the current delta is the direction from A to B, multiplied by the strength direction meaning B minus A, instead of minus A */ voiceWorkbenchSaveButton.addEventListener("click", () => { const voiceName = voiceWorkbenchVoiceNameInput.value const voiceId = voiceWorkbenchVoiceIDInput.value const gender = voiceWorkbenchGenderDropdown.value const author = voiceWorkbenchAuthorInput.value || "Anonymous" const lang = voiceWorkbenchLanguageDropdown.value if (!voiceName.trim().length) { return window.errorModal(window.i18n.ENTER_VOICE_NAME) } if (!voiceId.trim().length) { return window.errorModal(window.i18n.ENTER_VOICE_ID) } const modelJson = { "version": "3.0", "modelVersion": "3.0", "modelType": "xVAPitch", "author": author, "lang": lang, "embOverABaseModel": voiceWorkbenchModelDropdown.value, "games": [ { "gameId": voiceWorkbenchGamesDropdown.value, "voiceId": voiceId, "variant": "Default", "voiceName": voiceName, "base_speaker_emb": window.voiceWorkbenchState.currentEmb, "gender": gender } ] } const gameModelsPath = `${window.userSettings[`modelspath_${voiceWorkbenchGamesDropdown.value}`]}` const jsonDestination = `${gameModelsPath}/${voiceId}.json` fs.writeFileSync(jsonDestination, JSON.stringify(modelJson, null, 4)) doSynth(JSON.stringify({ sequence: " This is what my voice sounds like. ", useCleanup: true, // TODO, user setting? base_lang: lang, base_emb: window.voiceWorkbenchState.currentEmb.join(","), outfile: jsonDestination.replace(".json", ".wav") })).then(() => { voiceWorkbenchDeleteButton.disabled = false window.currentModel = undefined generateVoiceButton.dataset.modelQuery = null window.infoModal(window.i18n.VOICE_CREATED_AT.replace("_1", jsonDestination)) // Clean up the temp file from the clean-up post-processing, if it exists if (fs.existsSync(jsonDestination.replace(".json", "_preCleanup.wav"))) { fs.unlinkSync(jsonDestination.replace(".json", "_preCleanup.wav")) } window.loadAllModels().then(() => { window.refreshExistingCraftedVoices() // Refresh the main page voice models if the same game is loaded as the target game models directory as saved into if (window.currentGame.gameId==voiceWorkbenchGamesDropdown.value) { window.changeGame(window.currentGame) window.refreshExistingCraftedVoices() } }) }) }) voiceWorkbenchGamesDropdown.addEventListener("change", () => { const gameModelsPath = `${window.userSettings[`modelspath_${voiceWorkbenchGamesDropdown.value}`]}` const voiceId = voiceWorkbenchVoiceIDInput.value const jsonLocation = `${gameModelsPath}/${voiceId}.json` voiceWorkbenchDeleteButton.disabled = !fs.existsSync(jsonLocation) }) voiceWorkbenchVoiceIDInput.addEventListener("change", () => { const gameModelsPath = `${window.userSettings[`modelspath_${voiceWorkbenchGamesDropdown.value}`]}` const voiceId = voiceWorkbenchVoiceIDInput.value const jsonLocation = `${gameModelsPath}/${voiceId}.json` voiceWorkbenchDeleteButton.disabled = !fs.existsSync(jsonLocation) }) voiceWorkbenchDeleteButton.addEventListener("click", () => { const gameModelsPath = `${window.userSettings[`modelspath_${voiceWorkbenchGamesDropdown.value}`]}` const voiceId = voiceWorkbenchVoiceIDInput.value const jsonLocation = `${gameModelsPath}/${voiceId}.json` window.confirmModal(window.i18n.CONFIRM_DELETE_CRAFTED_VOICE.replace("_1", voiceWorkbenchVoiceNameInput.value).replace("_2", jsonLocation)).then(resp => { if (resp) { if (fs.existsSync(jsonLocation.replace(".json", ".wav"))) { fs.unlinkSync(jsonLocation.replace(".json", ".wav")) } fs.unlinkSync(jsonLocation) } window.infoModal(window.i18n.SUCCESSFULLY_DELETED_CRAFTED_VOICE) window.loadAllModels().then(() => { // Refresh the main page voice models if the same game is loaded as the target game models directory deleted from if (window.currentGame.gameId==voiceWorkbenchGamesDropdown.value) { window.changeGame(window.currentGame) window.refreshExistingCraftedVoices() } voiceWorkbenchCancelButton.click() }) }) }) voiceWorkbenchCancelButton.addEventListener("click", () => { window.voiceWorkbenchState.isStarted = false voiceWorkbenchLoadedContent.style.display = "none" voiceWorkbenchLoadedContent2.style.display = "none" voiceWorkbenchStartButton.style.display = "flex" window.voiceWorkbenchLoadOrResetCraftedVoice() })