Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
| const path = require('path') | |
| const smi = require('node-nvidia-smi') | |
| window.batch_state = { | |
| lines: [], | |
| fastModeActuallyFinishedTasks: 0, | |
| fastModeOutputPromises: [], | |
| lastModel: undefined, | |
| lastVocoder: undefined, | |
| lineIndex: 0, | |
| state: false, | |
| outPathsChecked: [], | |
| skippedExisting: 0, | |
| paginationIndex: 0, | |
| taskBarPercent: 0, | |
| startTime: undefined, | |
| linesDoneSinceStart: 0 | |
| } | |
| // https://stackoverflow.com/questions/1293147/example-javascript-code-to-parse-csv-data | |
| function CSVToArray( strData, strDelimiter ){ | |
| // Check to see if the delimiter is defined. If not, | |
| // then default to comma. | |
| strDelimiter = (strDelimiter || ","); | |
| // Create a regular expression to parse the CSV values. | |
| var objPattern = new RegExp( | |
| ( | |
| // Delimiters. | |
| "(\\" + strDelimiter + "|\\r?\\n|\\r|^)" + | |
| // Quoted fields. | |
| "(?:\"([^\"]*(?:\"\"[^\"]*)*)\"|" + | |
| // Standard fields. | |
| "([^\"\\" + strDelimiter + "\\r\\n]*))" | |
| ), | |
| "gi" | |
| ); | |
| // Create an array to hold our data. Give the array | |
| // a default empty first row. | |
| var arrData = [[]]; | |
| // Create an array to hold our individual pattern | |
| // matching groups. | |
| var arrMatches = null; | |
| // Keep looping over the regular expression matches | |
| // until we can no longer find a match. | |
| while (arrMatches = objPattern.exec( strData )){ | |
| // Get the delimiter that was found. | |
| var strMatchedDelimiter = arrMatches[ 1 ]; | |
| // Check to see if the given delimiter has a length | |
| // (is not the start of string) and if it matches | |
| // field delimiter. If id does not, then we know | |
| // that this delimiter is a row delimiter. | |
| if ( | |
| strMatchedDelimiter.length && | |
| strMatchedDelimiter !== strDelimiter | |
| ){ | |
| // Since we have reached a new row of data, | |
| // add an empty row to our data array. | |
| arrData.push( [] ); | |
| } | |
| var strMatchedValue; | |
| // Now that we have our delimiter out of the way, | |
| // let's check to see which kind of value we | |
| // captured (quoted or unquoted). | |
| if (arrMatches[ 2 ]){ | |
| // We found a quoted value. When we capture | |
| // this value, unescape any double quotes. | |
| strMatchedValue = arrMatches[ 2 ].replace( | |
| new RegExp( "\"\"", "g" ), | |
| "\"" | |
| ); | |
| } else { | |
| // We found a non-quoted value. | |
| strMatchedValue = arrMatches[ 3 ]; | |
| } | |
| // Now that we have our value string, let's add | |
| // it to the data array. | |
| arrData[ arrData.length - 1 ].push( strMatchedValue ); | |
| } | |
| // Return the parsed data. | |
| return( arrData ); | |
| } | |
| window.CSVToArray = CSVToArray | |
| let smiInterval = setInterval(() => { | |
| try { | |
| if (window.userSettings.useGPU) { | |
| smi((err, data) => { | |
| if (err) { | |
| console.log("smi error: ", err) | |
| } | |
| if (data && data.nvidia_smi_log.cuda_version) { | |
| let total | |
| let used | |
| if (data.nvidia_smi_log.gpu.length) { | |
| total = parseInt(data.nvidia_smi_log.gpu[0].fb_memory_usage.total.split(" ")[0]) | |
| used = parseInt(data.nvidia_smi_log.gpu[0].fb_memory_usage.used.split(" ")[0]) | |
| } else { | |
| total = parseInt(data.nvidia_smi_log.gpu.fb_memory_usage.total.split(" ")[0]) | |
| used = parseInt(data.nvidia_smi_log.gpu.fb_memory_usage.used.split(" ")[0]) | |
| } | |
| const percent = used/total*100 | |
| vramUsage.innerHTML = `${(used/1000).toFixed(1)}/${(total/1000).toFixed(1)} GB (${percent.toFixed(2)}%)` | |
| } | |
| }) | |
| } else { | |
| vramUsage.innerHTML = window.i18n.NOT_USING_GPU | |
| } | |
| } catch (e) { | |
| console.log(e) | |
| window.appLogger.log(e.stack) | |
| clearInterval(smiInterval) | |
| } | |
| }, 1000) | |
| batch_instructions_btn.addEventListener("click", () => { | |
| window.createModal("error", `${window.i18n.BATCH_INSTR1} <a href="https://www.youtube.com/watch?v=PK-m54f84q4" target="_blank">${window.i18n.BATCH_INSTR2}</a> <span>${window.i18n.BATCH_INSTR3}</span>`) | |
| }) | |
| batch_generateSample.addEventListener("click", () => { | |
| // const lines = [] | |
| const csv = ["game_id,voice_id,text,vocoder,out_path,pacing"] // TODO: ffmpeg options | |
| const games = Object.keys(window.games) | |
| if (games.length==0) { | |
| window.errorModal(window.i18n.BATCH_ERR_NO_VOICES) | |
| return | |
| } | |
| const sampleText = [ | |
| "Include as many lines of text you wish with one line of data per line of voice to be read out.", | |
| "Make sure that the required columns (game_id, voice_id, and text) are filled out", | |
| "The others can be left blank, and the app will figure out some sensible defaults", | |
| "The valid options for vocoder are one of: quickanddirty, waveglow, waveglowBIG, hifi", | |
| "If your specified model does not have a bespoke hifi model, it will use the waveglow model, also the default if you leave this blank.", | |
| "For all the other options, you can leave them blank.", | |
| "If no output path is specified for a specific voice, the default batch output directory will be used", | |
| ] | |
| sampleText.forEach(line => { | |
| const game = games[parseInt(Math.random()*games.length)] | |
| const gameModels = window.games[game].models | |
| const model = gameModels[parseInt(Math.random()*gameModels.length)].variants[0] | |
| console.log("game", game, "model", model) | |
| const record = { | |
| game_id: game, | |
| voice_id: model.voiceId, | |
| text: line, | |
| vocoder: ["quickanddirty","waveglow","waveglowBIG","hifi"][parseInt(Math.random()*4)], | |
| out_path: "", | |
| pacing: 1 | |
| } | |
| // lines.push(record) | |
| csv.push(Object.values(record).map(v => typeof v =="string" ? `"${v}"` : v).join(",")) | |
| }) | |
| const out_directory = `${__dirname.replace("\\javascript", "").replace(/\\/g,"/")}/batch`.replace(/\/\//g, "/").replace("resources/app/resources/app", "resources/app") | |
| if (!fs.existsSync(out_directory)){ | |
| fs.mkdirSync(out_directory) | |
| } | |
| fs.writeFileSync(`${out_directory}/sample.csv`, csv.join("\n")) | |
| // shell.showItemInFolder(`${out_directory}/sample.csv`) | |
| er.shell.showItemInFolder(`${out_directory}/sample.csv`) | |
| spawn(`explorer`, [out_directory], {stdio: "ignore"}) | |
| }) | |
| window.readFileTxt = (file) => { | |
| return new Promise((resolve, reject) => { | |
| const dataLines = [] | |
| const reader = new FileReader() | |
| reader.readAsText(file) | |
| reader.onloadend = () => { | |
| const lines = reader.result.replace(/\r\n/g, "\n").split("\n") | |
| lines.forEach(line => { | |
| if (line.trim().length) { | |
| const record = {} | |
| record.game_id = window.currentModel.games[0].gameId | |
| record.voice_id = window.currentModel.games[0].voiceId | |
| record.text = line | |
| if (window.currentModel.hifi) { | |
| record.vocoder = "hifi" | |
| } | |
| dataLines.push(record) | |
| } | |
| }) | |
| resolve(dataLines) | |
| } | |
| }) | |
| } | |
| window.readFile = (file) => { | |
| return new Promise((resolve, reject) => { | |
| const dataLines = [] | |
| const reader = new FileReader() | |
| reader.readAsText(file) | |
| reader.onloadend = () => { | |
| const lines = reader.result.replace(/\r\n/g, "\n").split("\n") | |
| const headerLine = lines.shift() | |
| const doRest = () => { | |
| const header = headerLine.split(window.userSettings.batch_delimiter).map(head => head.replace(/\r/, "")) | |
| lines.forEach(line => { | |
| const record = {} | |
| if (line.trim().length) { | |
| const parts = CSVToArray(line, window.userSettings.batch_delimiter)[0] | |
| parts.forEach((val, vi) => { | |
| try { | |
| let header_val = header[vi].replace(/^"/, "").replace(/"$/, "") | |
| record[header_val.replace(/\s/g,"")] = (val||"").replace(/\\/g, "/") | |
| } catch (e) { | |
| window.errorModal(`Error parsing line: ${val}`) | |
| console.log(e) | |
| window.appLogger.log(e) | |
| } | |
| }) | |
| dataLines.push(record) | |
| } | |
| }) | |
| resolve(dataLines) | |
| } | |
| const headerLine_clean = headerLine.replaceAll("\"", "") | |
| if (headerLine_clean.includes(window.userSettings.batch_delimiter)) { | |
| doRest() | |
| } else { | |
| const potentialDelimiter = headerLine_clean.split("voice_id")[0].split("game_id")[1] | |
| window.confirmModal(window.i18n.BATCH_CHANGE_DELIMITER.replace("_1", window.userSettings.batch_delimiter).replace("_2", potentialDelimiter)).then(response => { | |
| if (response) { | |
| window.userSettings.batch_delimiter = potentialDelimiter | |
| setting_batch_delimiter.value = potentialDelimiter | |
| window.saveUserSettings() | |
| doRest() | |
| } | |
| }) | |
| } | |
| } | |
| }) | |
| } | |
| let handleMetadataCSVdrop_response = undefined | |
| const sleep = ms => { | |
| return new Promise(resolve => { | |
| setTimeout(() => { | |
| resolve() | |
| }, ms) | |
| }) | |
| } | |
| const handleMetadataCSVdrop = (file) => { | |
| return new Promise(async resolve => { | |
| i18n_batch_metadata_open_btn.click() | |
| handleMetadataCSVdrop_response = undefined | |
| batch_metadata_input_gameID.value = window.currentGame.gameId | |
| if (window.currentModel) { | |
| batch_metadata_input_voiceID.value = window.currentModel.voiceId | |
| } | |
| await sleep(1000) | |
| while (handleMetadataCSVdrop_response === undefined) { | |
| if (batchMetadataCSVContainer.style.opacity.length==0 || parseFloat(batchMetadataCSVContainer.style.opacity)==1) { | |
| await sleep(1000) | |
| } else { | |
| handleMetadataCSVdrop_response = false | |
| } | |
| } | |
| if (handleMetadataCSVdrop_response) { | |
| const dataLines = [] | |
| const reader = new FileReader() | |
| reader.readAsText(file) | |
| reader.onloadend = () => { | |
| const lines = reader.result.replace(/\r\n/g, "\n").split("\n") | |
| lines.forEach(line => { | |
| if (line.trim().length) { | |
| const text = line.split("|")[1] | |
| const record = {} | |
| record.game_id = batch_metadata_input_gameID.value | |
| record.voice_id = batch_metadata_input_voiceID.value | |
| record.text = text | |
| if (!window.currentModel || window.currentModel.hifi) { | |
| record.vocoder = "hifi" | |
| } | |
| dataLines.push(record) | |
| } | |
| }) | |
| resolve(dataLines) | |
| } | |
| } else { | |
| resolve(false) | |
| } | |
| }) | |
| } | |
| i18n_batch_metadata_confirm_btn.addEventListener("click", () => { | |
| handleMetadataCSVdrop_response = true | |
| batchMetadataCSVContainer.click() | |
| batchIcon.click() | |
| }) | |
| window.uploadBatchCSVs = async (eType, event) => { | |
| if (["dragenter", "dragover"].includes(eType)) { | |
| batch_main.style.background = "#5b5b5b" | |
| batch_main.style.color = "white" | |
| } | |
| if (["dragleave", "drop"].includes(eType)) { | |
| batch_main.style.background = "#4b4b4b" | |
| batch_main.style.color = "gray" | |
| } | |
| event.preventDefault() | |
| event.stopPropagation() | |
| const dataLines = [] | |
| if (eType=="drop") { | |
| batchDropZoneNote.innerHTML = window.i18n.PROCESSING_DATA | |
| window.batch_state.skippedExisting = 0 | |
| const dataTransfer = event.dataTransfer | |
| const files = Array.from(dataTransfer.files) | |
| for (let fi=0; fi<files.length; fi++) { | |
| const file = files[fi] | |
| if (!file.name.toLowerCase().endsWith(".csv") || file.name.toLowerCase()=="metadata.csv") { | |
| if ( file.name.toLowerCase().endsWith(".txt") || file.name.toLowerCase()=="metadata.csv" ) { | |
| if (window.currentModel || file.name.toLowerCase()=="metadata.csv") { | |
| window.appLogger.log(`Reading file: ${file.name}`) | |
| let records | |
| if (file.name.toLowerCase()=="metadata.csv") { | |
| records = await handleMetadataCSVdrop(file) | |
| if (records===false) { | |
| continue | |
| } | |
| } else { | |
| if (window.currentModel) { | |
| records = await window.readFileTxt(file) | |
| } | |
| } | |
| if (window.currentModel || file.name.toLowerCase()=="metadata.csv") { | |
| if (window.userSettings.batch_skipExisting) { | |
| window.appLogger.log("Checking existing files before adding to queue") | |
| } else { | |
| window.appLogger.log("Adding files to queue") | |
| } | |
| records.forEach(item => { | |
| let outPath | |
| if (item.out_path && item.out_path.split("/").reverse()[0].includes(".")) { | |
| outPath = item.out_path | |
| } else { | |
| if (item.out_path) { | |
| outPath = item.out_path | |
| } else { | |
| outPath = window.userSettings.batchOutFolder | |
| } | |
| if (item.vc_content) { | |
| let vc_content_fname = item.vc_content.split("/").reverse()[0] | |
| outPath = `${outPath}/${vc_content_fname.slice(0,vc_content_fname.length-4).slice(0, window.userSettings.max_filename_chars-10).replace(/\.$/, "")}.${window.userSettings.audio.format}` | |
| } else { | |
| outPath = `${outPath}/${item.voice_id}_${item.vocoder}_${item.text.replace(/[\/\\:\*?<>"|]*/g, "").slice(0, window.userSettings.max_filename_chars-10).replace(/\.$/, "")}.${window.userSettings.audio.format}` | |
| } | |
| } | |
| outPath = outPath.startsWith("./") ? window.userSettings.batchOutFolder + outPath.slice(1,100000) : outPath | |
| item.out_path = outPath | |
| if (window.userSettings.batch_skipExisting && fs.existsSync(outPath)) { | |
| window.batch_state.skippedExisting++ | |
| } else { | |
| dataLines.push(item) | |
| } | |
| }) | |
| } | |
| } | |
| continue | |
| } else { | |
| continue | |
| } | |
| } | |
| window.appLogger.log(`Reading file: ${file.name}`) | |
| const records = await window.readFile(file) | |
| if (window.userSettings.batch_skipExisting) { | |
| window.appLogger.log("Checking existing files before adding to queue") | |
| } else { | |
| window.appLogger.log("Adding files to queue") | |
| } | |
| records.forEach((item, ii) => { | |
| let outPath | |
| if (item.out_path && item.out_path.split("/").reverse()[0].includes(".")) { | |
| outPath = item.out_path | |
| } else { | |
| if (item.out_path) { | |
| outPath = item.out_path | |
| } else { | |
| outPath = window.userSettings.batchOutFolder | |
| } | |
| if (item.vc_content) { | |
| let vc_content_fname = item.vc_content.split("/").reverse()[0] | |
| outPath = `${outPath}/${vc_content_fname.slice(0,vc_content_fname.length-4).slice(0,item.vc_content.length-4).slice(0, window.userSettings.max_filename_chars-10).replace(/\.$/, "")}.${window.userSettings.audio.format}` | |
| } else { | |
| outPath = `${outPath}/${item.voice_id}_${item.vocoder}_${item.text.replace(/[\/\\:\*?<>"|]*/g, "").slice(0, window.userSettings.max_filename_chars-10).replace(/\.$/, "")}.${window.userSettings.audio.format}` | |
| } | |
| } | |
| outPath = outPath.startsWith("./") ? window.userSettings.batchOutFolder + outPath.slice(1,100000) : outPath | |
| item.out_path = outPath | |
| if (window.userSettings.batch_skipExisting && fs.existsSync(outPath)) { | |
| window.batch_state.skippedExisting++ | |
| } else { | |
| dataLines.push(item) | |
| } | |
| }) | |
| } | |
| if (dataLines.length==0 && window.batch_state.skippedExisting) { | |
| batchDropZoneNote.innerHTML = window.i18n.BATCH_DROPZONE | |
| return window.errorModal(window.i18n.BATCH_ERR_SKIPPEDALL.replace("_1", window.batch_state.skippedExisting)) | |
| } | |
| window.batch_state.paginationIndex = 0 | |
| batch_pageNum.value = 1 | |
| window.appLogger.log("Preprocessing data...") | |
| const cleanedData = window.preProcessCSVData(dataLines) | |
| if (cleanedData.length) { | |
| window.populateBatchRecordsList(cleanedData) | |
| window.appLogger.log("Grouping up lines...") | |
| const finalOrder = window.groupBatchLines() | |
| window.refreshBatchRecordsList(finalOrder) | |
| window.batch_state.lines = finalOrder | |
| } else { | |
| // batch_clearBtn.click() | |
| } | |
| window.appLogger.log("batch import done") | |
| const numPages = Math.ceil(window.batch_state.lines.length/window.userSettings.batch_paginationSize) | |
| batch_total_pages.innerHTML = `of ${numPages}` | |
| batchDropZoneNote.innerHTML = window.i18n.BATCH_DROPZONE | |
| } | |
| } | |
| window.preProcessCSVData = data => { | |
| batch_main.style.display = "block" | |
| batchDropZoneNote.style.display = "none" | |
| batchRecordsHeader.style.display = "flex" | |
| batch_clearBtn.style.display = "inline-block" | |
| Array.from(batchRecordsHeader.children).forEach(item => item.style.backgroundColor = `#${window.currentGame.themeColourPrimary}`) | |
| const availableGames = Object.keys(window.games) | |
| for (let di=0; di<data.length; di++) { | |
| try { | |
| const record = data[di] | |
| // Validate the records first | |
| // ================== | |
| if (!record.game_id) { | |
| window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: ${window.i18n.MISSING} game_id`) | |
| return [] | |
| } | |
| if (!record.voice_id) { | |
| window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: ${window.i18n.MISSING} voice_id`) | |
| return [] | |
| } | |
| if ((!record.text || record.text.length==0) && (!record.vc_content)) { | |
| window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: ${window.i18n.MISSING} text/vc_content`) | |
| return [] | |
| } | |
| // Check that the game_id exists | |
| if (!availableGames.includes(record.game_id)) { | |
| window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: game_id "${record.game_id}" ${window.i18n.BATCH_ERR_GAMEID} <br><br>(${availableGames.join(', ')})`) | |
| return [] | |
| } | |
| // Check that the voice_id exists | |
| const gameVoices = [] | |
| window.games[record.game_id].models.forEach(model => { | |
| model.variants.forEach(variant => gameVoices.push(variant.voiceId)) | |
| }) | |
| if (!gameVoices.includes(record.voice_id)) { | |
| window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: voice_id "${record.voice_id}" ${window.i18n.BATCH_ERR_VOICEID}: ${record.game_id}`) | |
| return [] | |
| } | |
| // Check that the vocoder exists | |
| if (!["quickanddirty", "waveglow", "waveglowBIG", "hifi", "", "-", undefined].includes(record.vocoder)) { | |
| window.errorModal(`[${window.i18n.LINE}: ${di+2}] ${window.i18n.ERROR}: ${window.i18n.BATCHH_VOCODER} "${record.vocoder}" ${window.i18n.BATCH_ERR_VOCODER1}: quickanddirty, waveglow, waveglowBIG, hifi ${window.i18n.BATCH_ERR_VOCODER2}`) | |
| return [] | |
| } | |
| data[di].modelType = undefined | |
| let hasHifi = false | |
| window.games[data[di].game_id].models.forEach(model => { | |
| model.variants.forEach(variant => { | |
| if (variant.voiceId==data[di].voice_id) { | |
| data[di].modelType = variant.modelType || model.modelType | |
| record.voiceName = model.voiceName // For easy access later on | |
| if (variant.hifi) { | |
| hasHifi = variant.hifi | |
| } | |
| if (variant.lang && !record.lang) { | |
| record.lang = "en" | |
| } | |
| // TODO allow batch mode voice conversion/speech to speech by computing the embs of specified audio files instead of using the base voice embedding | |
| // Also TODO, might need to allow for custom voice embeddings | |
| if (variant.modelType=="xVAPitch") { | |
| record.base_emb = variant.base_speaker_emb | |
| record.vocoder = "-" | |
| } | |
| } | |
| }) | |
| }) | |
| // Fill with defaults | |
| // ================== | |
| if (!record.out_path) { | |
| record.out_path = window.userSettings.batchOutFolder | |
| } | |
| if (!record.pacing) { | |
| record.pacing = 1 | |
| } | |
| record.pacing = parseFloat(record.pacing) | |
| if (!record.pitch_amp) { | |
| record.pitch_amp = 1 | |
| } | |
| record.pitch_amp = parseFloat(record.pitch_amp) | |
| if (!record.out_path.includes(":/") && !record.out_path.startsWith("./")) { | |
| record.out_path = `./${record.out_path}` | |
| } | |
| if (!record.vocoder || (record.vocoder=="hifi" && !hasHifi)) { | |
| record.vocoder = "quickanddirty" | |
| } | |
| } catch (e) { | |
| console.log(e) | |
| window.appLogger.log(e) | |
| console.log(data[di]) | |
| console.log(window.games[data[di].game_id]) | |
| } | |
| } | |
| return data | |
| } | |
| window.populateBatchRecordsList = records => { | |
| batch_synthesizeBtn.style.display = "inline-block" | |
| batchDropZoneNote.style.display = "none" | |
| records.forEach((record, ri) => { | |
| const row = createElem("div") | |
| const rNumElem = createElem("div", batchRecordsContainer.children.length.toString()) | |
| const rStatusElem = createElem("div", "Ready") | |
| const rActionsElem = createElem("div") | |
| const rVoiceElem = createElem("div", record.voice_id) | |
| const rTextElem = createElem("div", (record.vc_content?record.vc_content:record.text).toString()) | |
| rTextElem.title = rTextElem.innerText | |
| if (record.vc_content) { | |
| rTextElem.style.fontStyle = "italic" | |
| } | |
| const rGameElem = createElem("div", record.game_id) | |
| const rOutPathElem = createElem("div", "‎"+record.out_path+"‎") | |
| rOutPathElem.title = record.out_path | |
| const rBaseLangElem = createElem("div", record.vc_content?"-":(record.lang||" ").toString()) | |
| const rVCStyleElem = createElem("div", (record.vc_style||" ").toString()) | |
| rVCStyleElem.title = rVCStyleElem.innerText | |
| const rVocoderElem = createElem("div", record.vocoder) | |
| const rPacingElem = createElem("div", record.vc_content?"-":(record.pacing||" ").toString()) | |
| const rPitchAmpElem = createElem("div", record.vc_content?"-":(record.pitch_amp||" ").toString()) | |
| row.appendChild(rNumElem) | |
| row.appendChild(rStatusElem) | |
| row.appendChild(rActionsElem) | |
| row.appendChild(rVoiceElem) | |
| row.appendChild(rTextElem) | |
| row.appendChild(rGameElem) | |
| row.appendChild(rOutPathElem) | |
| row.appendChild(rBaseLangElem) | |
| row.appendChild(rVCStyleElem) | |
| row.appendChild(rVocoderElem) | |
| row.appendChild(rPacingElem) | |
| row.appendChild(rPitchAmpElem) | |
| window.batch_state.lines.push([record, row, ri]) | |
| }) | |
| } | |
| window.refreshBatchRecordsList = (finalOrder) => { | |
| batchRecordsContainer.innerHTML = "" | |
| finalOrder = finalOrder ? finalOrder : window.batch_state.lines | |
| const startIndex = (window.batch_state.paginationIndex*window.userSettings.batch_paginationSize) | |
| const endIndex = Math.min(startIndex+window.userSettings.batch_paginationSize, finalOrder.length) | |
| for (let ri=startIndex; ri<endIndex; ri++) { | |
| const recordAndElem = finalOrder[ri] | |
| recordAndElem[1].children[0].innerHTML = (ri+1)//batchRecordsContainer.children.length.toString() | |
| batchRecordsContainer.appendChild(recordAndElem[1]) | |
| } | |
| window.toggleNumericalRecordsDisplay() | |
| } | |
| // Sort the lines by voice_id, and then by vocoder used | |
| window.groupBatchLines = () => { | |
| if (window.userSettings.batch_doGrouping) { | |
| const voices_order = [] | |
| const lines = window.batch_state.lines.sort((a,b) => { | |
| return a.voice_id - b.voice_id | |
| }) | |
| const voices_groups = {} | |
| // Get the order of the voice_id, and group them up | |
| window.batch_state.lines.forEach(record => { | |
| if (!voices_order.includes(record[0].voice_id)) { | |
| voices_order.push(record[0].voice_id) | |
| voices_groups[record[0].voice_id] = [] | |
| } | |
| voices_groups[record[0].voice_id].push(record) | |
| }) | |
| // Go through the voice groups and sort them by vocoder | |
| if (window.userSettings.batch_doVocoderGrouping) { | |
| voices_order.forEach(voice_id => { | |
| voices_groups[voice_id] = voices_groups[voice_id].sort((a,b) => a[0].vocoder<b[0].vocoder?1:-1) | |
| }) | |
| } | |
| // Collate everything back into the final order | |
| const finalOrder = [] | |
| voices_order.forEach(voice_id => { | |
| voices_groups[voice_id].forEach(record => finalOrder.push(record)) | |
| }) | |
| return finalOrder | |
| } else { | |
| return window.batch_state.lines | |
| } | |
| } | |
| batch_clearBtn.addEventListener("click", () => { | |
| window.batch_state.lines = [] | |
| batch_main.style.display = "flex" | |
| batchDropZoneNote.style.display = "block" | |
| batchRecordsHeader.style.display = "none" | |
| batch_clearBtn.style.display = "none" | |
| batch_outputFolderInput.style.display = "inline-block" | |
| batch_clearDirOpts.style.display = "flex" | |
| batch_skipExistingOpts.style.display = "flex" | |
| batch_useSR.style.display = "flex" | |
| batch_useCleanup.style.display = "flex" | |
| batch_outputNumericallyOpts.style.display = "flex" | |
| batch_progressItems.style.display = "none" | |
| batch_progressBar.style.display = "none" | |
| batch_pauseBtn.style.display = "none" | |
| batch_stopBtn.style.display = "none" | |
| batch_synthesizeBtn.style.display = "none" | |
| batchRecordsContainer.innerHTML = "" | |
| }) | |
| window.startBatch = () => { | |
| // Output directory | |
| if (!fs.existsSync(window.userSettings.batchOutFolder)) { | |
| window.userSettings.batchOutFolder.split("/") | |
| .reduce((prevPath, folder) => { | |
| const currentPath = path.join(prevPath, folder, path.sep); | |
| if (!fs.existsSync(currentPath)){ | |
| try { | |
| fs.mkdirSync(currentPath); | |
| } catch (e) { | |
| window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}:<br><br>`+e.message) | |
| throw "" | |
| } | |
| } | |
| return currentPath; | |
| }, ''); | |
| } | |
| if (batch_clearDirFirstCkbx.checked) { | |
| const filesAndFolders = fs.readdirSync(window.userSettings.batchOutFolder) | |
| filesAndFolders.forEach(faf => { | |
| // Ignore .csv and .txt files | |
| if (faf.toLowerCase().endsWith(".csv") || faf.toLowerCase().endsWith(".txt")) { | |
| return | |
| } | |
| if (fs.lstatSync(`${window.userSettings.batchOutFolder}/${faf}`).isDirectory()) { | |
| window.deleteFolderRecursive(`${window.userSettings.batchOutFolder}/${faf}`, false) | |
| } else { | |
| fs.unlinkSync(`${window.userSettings.batchOutFolder}/${faf}`) | |
| } | |
| console.log(`${window.userSettings.batchOutFolder}/${faf}`, ) | |
| }) | |
| } | |
| batch_synthesizeBtn.style.display = "none" | |
| batch_clearBtn.style.display = "none" | |
| batch_outputFolderInput.style.display = "none" | |
| batch_clearDirOpts.style.display = "none" | |
| batch_skipExistingOpts.style.display = "none" | |
| batch_useSR.style.display = "none" | |
| batch_useCleanup.style.display = "none" | |
| batch_outputNumericallyOpts.style.display = "none" | |
| batch_progressItems.style.display = "flex" | |
| batch_progressBar.style.display = "flex" | |
| batch_pauseBtn.style.display = "inline-block" | |
| batch_stopBtn.style.display = "inline-block" | |
| batch_openDirBtn.style.display = "none" | |
| window.batch_state.lines.forEach(record => { | |
| record[1].children[1].innerHTML = window.i18n.READY | |
| record[1].children[1].style.background = "none" | |
| }) | |
| window.batch_state.fastModeOutputPromises = [] | |
| window.batch_state.fastModeActuallyFinishedTasks = 0 | |
| window.batch_state.lineIndex = 0 | |
| window.batch_state.state = true | |
| window.batch_state.outPathsChecked = [] | |
| window.batch_state.startTime = new Date() | |
| window.batch_state.linesDoneSinceStart = 0 | |
| window.performSynthesis() | |
| } | |
| window.batchChangeVoice = (game, voice, modelType) => { | |
| return new Promise((resolve) => { | |
| if (!window.batch_state.state) { | |
| return resolve() | |
| } | |
| // Update the main app with any changes, if a voice has already been selected | |
| if (window.currentModel) { | |
| generateVoiceButton.innerHTML = window.i18n.LOAD_MODEL | |
| keepSampleButton.style.display = "none" | |
| wavesurferContainer.innerHTML = "" | |
| const modelGameFolder = window.currentModel.audioPreviewPath.split("/")[0] | |
| const modelFileName = window.currentModel.audioPreviewPath.split("/")[1].split(".wav")[0] | |
| generateVoiceButton.dataset.modelQuery = JSON.stringify({ | |
| outputs: parseInt(window.currentModel.outputs), | |
| model: `${window.path}/models/${modelGameFolder}/${modelFileName}`, | |
| model_speakers: window.currentModel.emb_size, | |
| cmudict: window.currentModel.cmudict | |
| }) | |
| } | |
| if (window.batch_state.state) { | |
| batch_progressNotes.innerHTML = `${window.i18n.BATCH_CHANGING_MODEL_TO}: ${voice}` | |
| } | |
| let model | |
| window.games[game].models.forEach(gameModel => { | |
| gameModel.variants.forEach(variant => { | |
| if (variant.voiceId==voice) { | |
| model = variant | |
| } | |
| }) | |
| }) | |
| doFetch(`http://localhost:8008/loadModel`, { | |
| method: "Post", | |
| body: JSON.stringify({ | |
| "modelType": modelType, | |
| "outputs": null, | |
| "model": `${window.userSettings[`modelspath_${game}`]}/${voice}`, | |
| "model_speakers": model.num_speakers, | |
| "base_lang": model.lang, | |
| "pluginsContext": JSON.stringify(window.pluginsContext) | |
| }) | |
| }).then(r=>r.text()).then(res => { | |
| resolve() | |
| }).catch(async e => { | |
| if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") { | |
| await window.batchChangeVoice(game, voice, modelType) | |
| resolve() | |
| } else { | |
| console.log(e) | |
| window.appLogger.log(e) | |
| batch_pauseBtn.click() | |
| if (document.getElementById("activeModal")) { | |
| activeModal.remove() | |
| } | |
| if (e.code=="ENOENT") { | |
| window.errorModal(window.i18n.ERR_SERVER) | |
| } else { | |
| window.errorModal(e.message) | |
| } | |
| resolve() | |
| } | |
| }) | |
| }) | |
| } | |
| window.batchChangeVocoder = (vocoder, game, voice) => { | |
| return new Promise((resolve) => { | |
| if (!window.batch_state.state) { | |
| return resolve() | |
| } | |
| console.log("Changing vocoder: ", vocoder) | |
| if (window.batch_state.state) { | |
| batch_progressNotes.innerHTML = `${window.i18n.BATCH_CHANGING_VOCODER_TO}: ${vocoder}` | |
| } | |
| const vocoderMappings = [["waveglow", "256_waveglow"], ["waveglowBIG", "big_waveglow"], ["quickanddirty", "qnd"], ["hifi", `${game}/${voice}.hg.pt`]] | |
| const vocoderId = vocoderMappings.find(record => record[0]==vocoder)[1] | |
| doFetch(`http://localhost:8008/setVocoder`, { | |
| method: "Post", | |
| body: JSON.stringify({ | |
| vocoder: vocoderId, | |
| modelPath: vocoderId=="256_waveglow" ? window.userSettings.waveglow_path : window.userSettings.bigwaveglow_path, | |
| }) | |
| }).then(r=>r.text()).then((res) => { | |
| if (res=="ENOENT") { | |
| closeModal(undefined, batchGenerationContainer).then(() => { | |
| setTimeout(() => { | |
| vocoder_select.value = window.userSettings.vocoder | |
| window.errorModal(`${window.i18n.BATCH_MODEL_NOT_FOUND}.${vocoderId.includes("waveglow")?" "+window.i18n.BATCH_DOWNLOAD_WAVEGLOW:""}`) | |
| batch_pauseBtn.click() | |
| resolve() | |
| }, 300) | |
| }) | |
| } else { | |
| window.batch_state.lastVocoder = vocoder | |
| resolve() | |
| } | |
| }).catch(async e => { | |
| if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") { | |
| await window.batchChangeVocoder(vocoder, game, voice) | |
| resolve() | |
| } else { | |
| console.log(e) | |
| window.appLogger.log(e) | |
| batch_pauseBtn.click() | |
| if (document.getElementById("activeModal")) { | |
| activeModal.remove() | |
| } | |
| if (e.code=="ENOENT") { | |
| window.errorModal(window.i18n.ERR_SERVER) | |
| } else { | |
| window.errorModal(e.message) | |
| } | |
| resolve() | |
| } | |
| }) | |
| }) | |
| } | |
| window.prepareLinesBatchForSynth = () => { | |
| const linesBatch = [] | |
| const records = [] | |
| let firstItemVoiceId = undefined | |
| let firstItemVocoder = undefined | |
| let speaker_i = 0 | |
| for (let i=0; i<Math.min(window.userSettings.batch_batchSize, window.batch_state.lines.length-window.batch_state.lineIndex); i++) { | |
| const record = window.batch_state.lines[window.batch_state.lineIndex+i] | |
| const vocoderMappings = [["waveglow", "256_waveglow"], ["waveglowBIG", "big_waveglow"], ["quickanddirty", "qnd"], ["hifi", `${record[0].game_id}/${record[0].voice_id}.hg.pt`]] | |
| const vocoder = record[0].vocoder=="-"?"-":vocoderMappings.find(voc => voc[0]==record[0].vocoder)[1] | |
| if (firstItemVoiceId==undefined) firstItemVoiceId = record[0].voice_id | |
| if (firstItemVocoder==undefined) firstItemVocoder = vocoder | |
| if (record[0].voice_id!=firstItemVoiceId || vocoder!=firstItemVocoder) { | |
| break | |
| } | |
| let model | |
| window.games[record[0].game_id].models.forEach(gamesModel => { | |
| gamesModel.variants.forEach(variant => { | |
| if (variant.voiceId==record[0].voice_id) { | |
| model = variant | |
| } | |
| }) | |
| }) | |
| const sequence = record[0].text | |
| const pitch = undefined // maybe later | |
| const duration = undefined // maybe later | |
| speaker_i = model.emb_i || 0 | |
| let pace = record[0].pacing | |
| pace = Number.isNaN(pace) ? 1.0 : pace | |
| let pitch_amp = record[0].pitch_amp | |
| pitch_amp = Number.isNaN(pitch_amp) ? 1.0 : pitch_amp | |
| const tempFileNum = `${Math.random().toString().split(".")[1]}` | |
| const tempFileLocation = `${window.path}/output/temp-${tempFileNum}.wav` | |
| let outPath | |
| let outFolder | |
| outPath = record[0].out_path | |
| outFolder = String(record[0].out_path).split("/").reverse().slice(1,10000).reverse().join("/") | |
| outFolder = outFolder.length ? outFolder : window.userSettings.batchOutFolder | |
| if (batch_outputNumerically.checked) { | |
| outPath = `${window.userSettings.batchOutFolder}/${String(record[2]).padStart(10, '0')}.${outPath.split(".").reverse()[0]}` | |
| } else { | |
| outPath = outPath.startsWith("./") ? window.userSettings.batchOutFolder + outPath.slice(1,100000) : outPath | |
| } | |
| linesBatch.push([sequence, pitch, duration, pace, tempFileLocation, outPath, outFolder, pitch_amp, record[0].lang, record[0].base_emb, record[0].vc_content, record[0].vc_style]) | |
| records.push(record) | |
| } | |
| return [speaker_i, firstItemVoiceId, firstItemVocoder, linesBatch, records] | |
| } | |
| window.addActionButtons = (records, ri) => { | |
| let audioPreview | |
| const playButton = createElem("button.smallButton", window.i18n.PLAY) | |
| playButton.style.background = `#${window.currentGame.themeColourPrimary}` | |
| playButton.addEventListener("click", () => { | |
| let audioPreviewPath = records[ri][0].fileOutputPath | |
| if (audioPreviewPath.startsWith("./")) { | |
| audioPreviewPath = window.userSettings.batchOutFolder + audioPreviewPath.replace("./", "/") | |
| } | |
| if (audioPreview==undefined) { | |
| const audioPreview = createElem("audio", {autoplay: false}, createElem("source", { | |
| src: audioPreviewPath | |
| })) | |
| audioPreview.addEventListener("play", () => { | |
| if (window.ctrlKeyIsPressed) { | |
| audioPreview.setSinkId(window.userSettings.alt_speaker) | |
| } else { | |
| audioPreview.setSinkId(window.userSettings.base_speaker) | |
| } | |
| }) | |
| audioPreview.setSinkId(window.userSettings.base_speaker) | |
| } | |
| }) | |
| records[ri][1].children[2].appendChild(playButton) | |
| // If not a Voice Conversion line | |
| if (!records[ri][0].vc_content) { | |
| const editButton = createElem("button.smallButton", window.i18n.EDIT) | |
| editButton.style.background = `#${window.currentGame.themeColourPrimary}` | |
| editButton.addEventListener("click", () => { | |
| audioPreview = undefined | |
| if (window.batch_state.state) { | |
| window.errorModal(window.i18n.BATCH_ERR_EDIT) | |
| return | |
| } | |
| // Change app theme to the voice's game | |
| if (window.currentGame.gameId!=records[ri][0].game_id) { | |
| window.changeGame(window.gameAssets[records[ri][0].game_id]) | |
| } | |
| dialogueInput.value = records[ri][0].text | |
| // Simulate voice loading through the UI | |
| if (!window.currentModel || window.currentModel.voiceId != records[ri][0].voice_id) { | |
| const voiceName = records[ri][0].voiceName | |
| const voiceButton = Array.from(voiceTypeContainer.children).find(button => button.innerHTML==voiceName) | |
| voiceButton.click() | |
| vocoder_select.value = records[ri][0].vocoder=="hifi" ? `${records[ri][0].game_id}/${records[ri][0].voice_id}.hg.pt` : records[ri][0].vocoder | |
| generateVoiceButton.click() | |
| } | |
| window.closeModal(batchGenerationContainer) | |
| setTimeout(() => { | |
| let audioPreviewPath = records[ri][0].fileOutputPath | |
| if (audioPreviewPath.startsWith("./")) { | |
| audioPreviewPath = window.userSettings.batchOutFolder + audioPreviewPath.replace("./", "/") | |
| } | |
| keepSampleButton.dataset.newFileLocation = "BATCH_EDIT"+audioPreviewPath | |
| generateVoiceButton.click() | |
| }, 500) | |
| }) | |
| records[ri][1].children[2].appendChild(editButton) | |
| } | |
| } | |
| window.batchKickOffMPffmpegOutput = (records, tempPaths, outPaths, options, extraInfo) => { | |
| let hasShownError = false | |
| return new Promise((resolve, reject) => { | |
| doFetch(`http://localhost:8008/batchOutputAudio`, { | |
| method: "Post", | |
| body: JSON.stringify({ | |
| input_paths: tempPaths, | |
| output_paths: outPaths, | |
| isBatchMode: true, | |
| pluginsContext: JSON.stringify(window.pluginsContext), | |
| processes: window.userSettings.batch_MPCount, | |
| extraInfo: extraInfo, | |
| options: JSON.stringify(options) | |
| }) | |
| }).then(r=>r.text()).then(res => { | |
| res = res.split("\n") | |
| res.forEach((resItem, ri) => { | |
| if (resItem.length && resItem!="-") { | |
| window.appLogger.log(`Batch error, item ${ri} - ${resItem}`) | |
| if (window.batch_state.state) { | |
| batch_pauseBtn.click() | |
| } | |
| window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}:<br><br>`+resItem) | |
| hasShownError = true | |
| records[ri][1].children[1].innerHTML = window.i18n.FAILED | |
| records[ri][1].children[1].style.background = "red" | |
| } else { | |
| records[ri][1].children[1].innerHTML = window.i18n.DONE | |
| records[ri][1].children[1].style.background = "green" | |
| fs.unlinkSync(tempPaths[ri]) | |
| window.addActionButtons(records, ri) | |
| } | |
| // if (!window.userSettings.batch_fastMode) { // No more fast modde. TODO, remove completely | |
| window.batch_state.lineIndex += 1 | |
| // } | |
| window.batch_state.fastModeActuallyFinishedTasks += 1 | |
| }) | |
| const percentDone = (window.batch_state.fastModeActuallyFinishedTasks) / window.batch_state.lines.length * 100 | |
| batch_progressBar.style.background = `linear-gradient(90deg, green ${parseInt(percentDone)}%, rgba(255,255,255,0) ${parseInt(percentDone)}%)` | |
| batch_progressBar.innerHTML = `${parseInt(percentDone* 100)/100}%` | |
| window.batch_state.taskBarPercent = percentDone/100 | |
| window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent) | |
| window.adjustETA() | |
| resolve() | |
| }).catch(async e => { | |
| if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") { | |
| await window.batchKickOffMPffmpegOutput(records, tempPaths, outPaths, options, extraInfo) | |
| resolve() | |
| } else { | |
| console.log(e) | |
| window.appLogger.log(e.stack) | |
| if (document.getElementById("activeModal")) { | |
| activeModal.remove() | |
| } | |
| if (!hasShownError) { | |
| window.errorModal(e.message) | |
| } | |
| resolve() | |
| } | |
| }) | |
| }) | |
| } | |
| window.batchKickOffFfmpegOutput = (ri, linesBatch, records, tempFileLocation, body) => { | |
| return new Promise((resolve, reject) => { | |
| doFetch(`http://localhost:8008/outputAudio`, { | |
| method: "Post", | |
| body | |
| }).then(r=>r.text()).then(res => { | |
| if (res.length && res!="-") { | |
| window.appLogger.log("res", res) | |
| if (window.batch_state.state) { | |
| batch_pauseBtn.click() | |
| } | |
| for (let ri2=ri; ri2<linesBatch.length; ri2++) { | |
| records[ri][1].children[1].innerHTML = window.i18n.FAILED | |
| records[ri][1].children[1].style.background = "red" | |
| } | |
| reject(res) | |
| } else { | |
| records[ri][1].children[1].innerHTML = window.i18n.DONE | |
| records[ri][1].children[1].style.background = "green" | |
| fs.unlinkSync(tempFileLocation) | |
| window.addActionButtons(records, ri) | |
| window.batch_state.fastModeActuallyFinishedTasks += 1 | |
| const percentDone = (window.batch_state.fastModeActuallyFinishedTasks) / window.batch_state.lines.length * 100 | |
| batch_progressBar.style.background = `linear-gradient(90deg, green ${parseInt(percentDone)}%, rgba(255,255,255,0) ${parseInt(percentDone)}%)` | |
| batch_progressBar.innerHTML = `${parseInt(percentDone* 100)/100}%` | |
| window.batch_state.taskBarPercent = percentDone/100 | |
| window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent) | |
| window.adjustETA() | |
| resolve() | |
| } | |
| }).catch(async e => { | |
| if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") { | |
| await window.batchKickOffFfmpegOutput(ri, linesBatch, records, tempFileLocation, body) | |
| resolve() | |
| } else { | |
| console.log(e) | |
| window.appLogger.log(e) | |
| batch_pauseBtn.click() | |
| if (document.getElementById("activeModal")) { | |
| activeModal.remove() | |
| } | |
| window.errorModal(e.message) | |
| resolve() | |
| } | |
| }) | |
| }) | |
| } | |
| window.batchKickOffGeneration = () => { | |
| return new Promise((resolve) => { | |
| if (!window.batch_state.state) { | |
| return resolve() | |
| } | |
| const [speaker_i, voice_id, vocoder, linesBatch, records] = window.prepareLinesBatchForSynth() | |
| records.forEach((record, ri) => { | |
| record[1].children[1].innerHTML = window.i18n.RUNNING | |
| record[1].children[1].style.background = "goldenrod" | |
| record[0].fileOutputPath = linesBatch[ri][5] | |
| }) | |
| const record = window.batch_state.lines[window.batch_state.lineIndex] | |
| if (window.batch_state.state) { | |
| if (linesBatch.length==1) { | |
| batch_progressNotes.innerHTML = `${window.i18n.SYNTHESIZING}: <i>${record[0].text}</i>` | |
| } else { | |
| batch_progressNotes.innerHTML = `${window.i18n.SYNTHESIZING} ${linesBatch.length} ${window.i18n.LINES}` | |
| } | |
| } | |
| const batchPostData = { | |
| modelType: records[0][0].modelType, | |
| batchSize: window.userSettings.batch_batchSize, | |
| defaultOutFolder: window.userSettings.batchOutFolder, | |
| pluginsContext: JSON.stringify(window.pluginsContext), | |
| outputJSON: window.userSettings.batch_json, | |
| useSR: batch_useSRCkbx.checked, | |
| useCleanup: batch_useCleanupCkbx.checked, | |
| speaker_i, vocoder, linesBatch | |
| } | |
| doFetch(`http://localhost:8008/synthesize_batch`, { | |
| method: "Post", | |
| body: JSON.stringify(batchPostData) | |
| }).then(r=>r.text()).then(async (res) => { | |
| if (res && res!="-") { | |
| if (res=="CUDA OOM") { | |
| window.errorModal(window.i18n.BATCH_ERR_CUDA_OOM) | |
| } else { | |
| window.errorModal(res.replace(/\n/g, "<br>")) | |
| } | |
| if (window.batch_state.state) { | |
| batch_pauseBtn.click() | |
| } | |
| return | |
| } | |
| // Create the output directory if it does not exist | |
| linesBatch.forEach(record => { | |
| let outFolder = record[6].startsWith("./") ? window.userSettings.batchOutFolder + record[6].slice(1,100000) : record[6] | |
| if (!window.batch_state.outPathsChecked.includes(outFolder)) { | |
| window.batch_state.outPathsChecked.push(outFolder) | |
| if (!fs.existsSync(outFolder)) { | |
| window.createFolderRecursive(outFolder) | |
| } | |
| } | |
| }) | |
| if (window.userSettings.audio.ffmpeg) { | |
| const options = { | |
| hz: window.userSettings.audio.hz, | |
| padStart: window.userSettings.audio.padStart, | |
| padEnd: window.userSettings.audio.padEnd, | |
| bit_depth: window.userSettings.audio.bitdepth, | |
| amplitude: window.userSettings.audio.amplitude, | |
| pitchMult: window.userSettings.audio.pitchMult, | |
| tempo: window.userSettings.audio.tempo, | |
| deessing: window.userSettings.audio.deessing, | |
| nr: window.userSettings.audio.nr, | |
| nf: window.userSettings.audio.nf, | |
| useNR: window.userSettings.audio.useNR, | |
| useSR: batch_useSRCkbx.checked, | |
| useCleanup: batch_useCleanupCkbx.checked, | |
| } | |
| if (window.batch_state.state) { | |
| batch_progressNotes.innerHTML = window.i18n.BATCH_OUTPUTTING_FFMPEG | |
| } | |
| const tempPaths = linesBatch.map(line => line[4]) | |
| const outPaths = linesBatch.map((line, li) => { | |
| let outPath = linesBatch[li][5].includes(":") || linesBatch[li][5].includes("./") ? linesBatch[li][5] : `${linesBatch[li][6]}/${linesBatch[li][5]}` | |
| if (outPath.startsWith("./")) { | |
| outPath = window.userSettings.batchOutFolder + outPath.slice(1,100000) | |
| } | |
| return outPath | |
| }) | |
| if (window.userSettings.batch_useMP) { | |
| const extraInfo = { | |
| game: records.map(rec => rec[0].game_id), | |
| voiceId: records.map(rec => rec[0].voice_id), | |
| voiceName: records.map(rec => rec[0].voiceName), | |
| inputSequence: records.map(rec => rec[0].text) | |
| } | |
| if (window.userSettings.batch_fastMode && false) { // No more fast mode. TODO, remove completely | |
| window.batch_state.fastModeOutputPromises.push(window.batchKickOffMPffmpegOutput(records, tempPaths, outPaths, options, JSON.stringify(extraInfo))) | |
| window.batch_state.lineIndex += records.length | |
| } else { | |
| await window.batchKickOffMPffmpegOutput(records, tempPaths, outPaths, options, JSON.stringify(extraInfo)) | |
| } | |
| } else { | |
| for (let ri=0; ri<linesBatch.length; ri++) { | |
| let tempFileLocation = tempPaths[ri] | |
| let outPath = outPaths[ri] | |
| try { | |
| if (window.batch_state.state) { | |
| records[ri][1].children[1].innerHTML = window.i18n.OUTPUTTING | |
| const extraInfo = { | |
| game: records[ri][0].game_id, | |
| voiceId: records[ri][0].voiceId, | |
| voiceName: records[ri][0].voiceName, | |
| letters: records[ri][0].text | |
| } | |
| if (window.userSettings.batch_fastMode && false) { // No more fast modde. TODO, remove completely | |
| window.batch_state.fastModeOutputPromises.push(window.batchKickOffFfmpegOutput(ri, linesBatch, records, tempFileLocation, JSON.stringify({ | |
| input_path: tempFileLocation, | |
| output_path: outPath, | |
| isBatchMode: true, | |
| pluginsContext: JSON.stringify(window.pluginsContext), | |
| extraInfo: JSON.stringify(extraInfo), | |
| options: JSON.stringify(options) | |
| }))) | |
| } else { | |
| await window.batchKickOffFfmpegOutput(ri, linesBatch, records, tempFileLocation, JSON.stringify({ | |
| input_path: tempFileLocation, | |
| output_path: outPath, | |
| isBatchMode: true, | |
| pluginsContext: JSON.stringify(window.pluginsContext), | |
| extraInfo: JSON.stringify(extraInfo), | |
| options: JSON.stringify(options) | |
| })) | |
| } | |
| window.batch_state.lineIndex += 1 | |
| } | |
| } catch (e) { | |
| console.log(e) | |
| window.errorModal(`${window.i18n.SOMETHING_WENT_WRONG}:<br><br>`+e) | |
| resolve() | |
| } | |
| } | |
| } | |
| window.batch_state.linesDoneSinceStart += linesBatch.length | |
| resolve() | |
| } else { | |
| linesBatch.forEach((lineRecord, li) => { | |
| let tempFileLocation = lineRecord[4] | |
| let outPath = lineRecord[5] | |
| try { | |
| fs.copyFileSync(tempFileLocation, outPath) | |
| records[li][1].children[1].innerHTML = window.i18n.DONE | |
| records[li][1].children[1].style.background = "green" | |
| window.batch_state.lineIndex += 1 | |
| window.addActionButtons(records, li) | |
| } catch (err) { | |
| console.log(err) | |
| window.appLogger.log(err) | |
| window.errorModal(err.message) | |
| batch_pauseBtn.click() | |
| } | |
| window.batch_state.linesDoneSinceStart += linesBatch.length | |
| resolve() | |
| }) | |
| } | |
| }).catch(async e => { | |
| if (e.code=="ECONNREFUSED" || e.code=="ECONNRESET") { | |
| await window.batchKickOffGeneration() | |
| resolve() | |
| } else { | |
| console.log(e) | |
| window.appLogger.log(e) | |
| batch_pauseBtn.click() | |
| if (document.getElementById("activeModal")) { | |
| activeModal.remove() | |
| } | |
| console.log(e.message) | |
| window.errorModal(e.message).then(() => resolve()) | |
| } | |
| }) | |
| }) | |
| } | |
| window.performSynthesis = async () => { | |
| if (batch_state.lineIndex-batch_state.fastModeActuallyFinishedTasks > window.userSettings.batch_fastModeMaxParallelizations) { | |
| console.log(`Ahead by ${batch_state.lineIndex-batch_state.fastModeActuallyFinishedTasks} tasks. Waiting...`) | |
| setTimeout(() => {window.performSynthesis()}, 1000) | |
| return | |
| } | |
| if (!window.batch_state.state) { | |
| return | |
| } | |
| if (window.batch_state.lineIndex==0) { | |
| const percentDone = (window.batch_state.lineIndex) / window.batch_state.lines.length * 100 | |
| batch_progressBar.style.background = `linear-gradient(90deg, green ${parseInt(percentDone)}%, rgba(255,255,255,0) ${parseInt(percentDone)}%)` | |
| batch_progressBar.innerHTML = `${parseInt(percentDone* 100)/100}%` | |
| window.batch_state.taskBarPercent = percentDone/100 | |
| window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent) | |
| } | |
| const record = window.batch_state.lines[window.batch_state.lineIndex] | |
| // Change the voice model if the next line uses a different one | |
| if (window.batch_state.lastModel!=record[0].voice_id) { | |
| await window.batchChangeVoice(record[0].game_id, record[0].voice_id, record[0].modelType) | |
| window.batch_state.lastModel = record[0].voice_id | |
| } | |
| // Change the vocoder if the next line uses a different one | |
| if (window.batch_state.lastVocoder!=record[0].vocoder && record[0].vocoder!="-") { | |
| await window.batchChangeVocoder(record[0].vocoder, record[0].game_id, record[0].voice_id) | |
| } | |
| await window.batchKickOffGeneration() | |
| if (window.batch_state.lineIndex==window.batch_state.lines.length) { | |
| // The end | |
| if (window.userSettings.batch_fastMode && false) { // No more fast modde. TODO, remove completely | |
| Promise.all(window.batch_state.fastModeOutputPromises).then(() => { | |
| window.stopBatch() | |
| batch_openDirBtn.style.display = "inline-block" | |
| }) | |
| } else { | |
| window.stopBatch() | |
| batch_openDirBtn.style.display = "inline-block" | |
| } | |
| } else { | |
| window.performSynthesis() | |
| } | |
| } | |
| window.pauseResumeBatch = () => { | |
| batch_progressNotes.innerHTML = window.i18n.PAUSED | |
| const isRunning = window.batch_state.state | |
| batch_pauseBtn.innerHTML = isRunning ? window.i18n.RESUME : window.i18n.PAUSE | |
| window.batch_state.state = !isRunning | |
| window.electronBrowserWindow.setProgressBar(window.batch_state.taskBarPercent?window.batch_state.taskBarPercent:1, {mode: isRunning ? "paused" : "normal"}) | |
| if (window.batch_state.state) { | |
| window.batch_state.startTime = new Date() | |
| window.batch_state.linesDoneSinceStart = 0 | |
| } | |
| if (!isRunning) { | |
| window.performSynthesis() | |
| } | |
| } | |
| window.stopBatch = (stoppedByUser) => { | |
| window.electronBrowserWindow.setProgressBar(0) | |
| window.batch_state.state = false | |
| window.batch_state.lineIndex = 0 | |
| batch_ETA_container.style.opacity = 0 | |
| batch_synthesizeBtn.style.display = "inline-block" | |
| batch_clearBtn.style.display = "inline-block" | |
| batch_outputFolderInput.style.display = "inline-block" | |
| batch_clearDirOpts.style.display = "flex" | |
| batch_skipExistingOpts.style.display = "flex" | |
| batch_useSR.style.display = "flex" | |
| batch_useCleanup.style.display = "flex" | |
| batch_outputNumericallyOpts.style.display = "flex" | |
| batch_progressItems.style.display = "none" | |
| batch_progressBar.style.display = "none" | |
| batch_pauseBtn.style.display = "none" | |
| batch_stopBtn.style.display = "none" | |
| window.batch_state.lines.forEach(record => { | |
| if (record[1].children[1].innerHTML==window.i18n.READY || record[1].children[1].innerHTML==window.i18n.RUNNING) { | |
| record[1].children[1].innerHTML = window.i18n.STOPPED | |
| record[1].children[1].style.background = "none" | |
| } | |
| }) | |
| const pluginData = { | |
| stoppedByUser: stoppedByUser | |
| } | |
| window.pluginsManager.runPlugins(window.pluginsManager.pluginsModules["batch-stop"]["post"], event="post batch-stop", pluginData) | |
| } | |
| window.adjustETA = () => { | |
| if (window.batch_state.state && window.batch_state.fastModeActuallyFinishedTasks>=2) { | |
| batch_ETA_container.style.opacity = 1 | |
| // Lines per second | |
| const timeNow = new Date() | |
| const timeSinceStart = timeNow - window.batch_state.startTime | |
| const avgMSTimePerLine = timeSinceStart / window.batch_state.fastModeActuallyFinishedTasks | |
| batch_eta_lps.innerHTML = parseInt((1000/avgMSTimePerLine)*100)/100 | |
| const remainingLines = window.batch_state.lines.length - window.batch_state.fastModeActuallyFinishedTasks | |
| let estTimeRemaining = avgMSTimePerLine*remainingLines | |
| // Estimated finish time | |
| const finishTime = new Date(timeNow.getTime() + estTimeRemaining) | |
| let etaFinishTime = `${finishTime.getHours()}:${String(finishTime.getMinutes()).padStart(2, "0")}:${String(finishTime.getSeconds()).padStart(2, "0")}` | |
| const days = [window.i18n.SUNDAY, window.i18n.MONDAY, window.i18n.TUESDAY, window.i18n.WEDNESDAY, window.i18n.THURSDAY, window.i18n.FRIDAY, window.i18n.SATURDAY] | |
| etaFinishTime = `${days[finishTime.getDay()]} ${etaFinishTime}` | |
| batch_eta_eta.innerHTML = etaFinishTime | |
| // Time remaining | |
| let etaTimeDisplay = [] | |
| if (estTimeRemaining > (1000*60*60)) { // hours | |
| const hours = parseInt(estTimeRemaining/(1000*60*60)) | |
| etaTimeDisplay.push(hours+"h") | |
| estTimeRemaining -= hours*(1000*60*60) | |
| } | |
| if (estTimeRemaining > (1000*60)) { // minutes | |
| const minutes = parseInt(estTimeRemaining/(1000*60)) | |
| etaTimeDisplay.push(String(minutes).padStart(2, "0")+"m") | |
| estTimeRemaining -= minutes*(1000*60) | |
| } | |
| if (estTimeRemaining > (1000)) { // seconds | |
| const seconds = parseInt(estTimeRemaining/(1000)) | |
| etaTimeDisplay.push(String(seconds).padStart(2, "0")+"s") | |
| estTimeRemaining -= seconds*(1000) | |
| } | |
| batch_eta_time.innerHTML = etaTimeDisplay.join(" ") | |
| } else { | |
| batch_ETA_container.style.opacity = 0 | |
| } | |
| } | |
| const openOutput = () => { | |
| er.shell.showItemInFolder(window.userSettings.batchOutFolder+"/dummy.txt") | |
| spawn(`explorer`, [window.userSettings.batchOutFolder.replace(/\//g, "\\")], {stdio: "ignore"}) | |
| } | |
| batch_paginationPrev.addEventListener("click", () => { | |
| batch_pageNum.value = Math.max(1, parseInt(batch_pageNum.value)-1) | |
| window.batch_state.paginationIndex = batch_pageNum.value-1 | |
| window.refreshBatchRecordsList() | |
| }) | |
| batch_paginationNext.addEventListener("click", () => { | |
| const numPages = Math.ceil(window.batch_state.lines.length/window.userSettings.batch_paginationSize) | |
| batch_pageNum.value = Math.min(parseInt(batch_pageNum.value)+1, numPages) | |
| window.batch_state.paginationIndex = batch_pageNum.value-1 | |
| window.refreshBatchRecordsList() | |
| }) | |
| batch_pageNum.addEventListener("change", () => { | |
| const numPages = Math.ceil(window.batch_state.lines.length/window.userSettings.batch_paginationSize) | |
| batch_pageNum.value = Math.max(1, Math.min(parseInt(batch_pageNum.value), numPages)) | |
| window.batch_state.paginationIndex = batch_pageNum.value-1 | |
| window.refreshBatchRecordsList() | |
| }) | |
| setting_batch_paginationSize.addEventListener("change", () => { | |
| const numPages = Math.ceil(window.batch_state.lines.length/window.userSettings.batch_paginationSize) | |
| batch_pageNum.value = Math.max(1, Math.min(parseInt(batch_pageNum.value), numPages)) | |
| window.batch_state.paginationIndex = batch_pageNum.value-1 | |
| batch_total_pages.innerHTML = window.i18n.PAGINATION_TOTAL_OF.replace("_1", numPages) | |
| window.refreshBatchRecordsList() | |
| }) | |
| window.toggleNumericalRecordsDisplay = () => { | |
| window.batch_state.lines.forEach(record => { | |
| record[1].children[6].innerHTML = batch_outputNumerically.checked ? `${window.userSettings.batchOutFolder}/${String(record[2]).padStart(10, '0')}` : record[0].out_path | |
| }) | |
| } | |
| batch_outputNumerically.addEventListener("click", () => { | |
| window.toggleNumericalRecordsDisplay() | |
| }) | |
| batch_saveToCSV.addEventListener("click", () => { | |
| try { | |
| const csv_file = [`game_id|voice_id|text`] | |
| window.batch_state.lines.forEach(line => { | |
| csv_file.push(`${line[0].game_id}|${line[0].voice_id}|${line[0].text}`) | |
| }) | |
| const outFileName = JSON.stringify(new Date()).replace("\"","").replaceAll(":","_").split(".")[0]+"_batch.csv" | |
| fs.writeFileSync(`${window.userSettings.batchOutFolder}/${outFileName}`, csv_file.join("\n"), "utf8") | |
| window.createModal("error", `${window.i18n.BATCH_TOCSV_DONE}<br><br>${window.userSettings.batchOutFolder}/${outFileName}`) | |
| } catch(e) { | |
| console.log(e) | |
| window.appLogger.log(e.stack) | |
| window.errorModal(e.stack) | |
| } | |
| }) | |
| batch_main.addEventListener("dragenter", event => window.uploadBatchCSVs("dragenter", event), false) | |
| batch_main.addEventListener("dragleave", event => window.uploadBatchCSVs("dragleave", event), false) | |
| batch_main.addEventListener("dragover", event => window.uploadBatchCSVs("dragover", event), false) | |
| batch_main.addEventListener("drop", event => window.uploadBatchCSVs("drop", event), false) | |
| batch_synthesizeBtn.addEventListener("click", window.startBatch) | |
| batch_pauseBtn.addEventListener("click", window.pauseResumeBatch) | |
| batch_stopBtn.addEventListener("click", () => window.stopBatch(true)) | |
| batch_openDirBtn.addEventListener("click", openOutput) |