"use strict" // This doesn't seem to work anymore. TODO, make it work again function getCaretPosition() { var sel = document.selection, range, rect; var x = 0, y = 0; if (sel) { if (sel.type != "Control") { range = sel.createRange(); range.collapse(true); x = range.boundingLeft; y = range.boundingTop; } } else if (window.getSelection) { sel = window.getSelection(); if (sel.rangeCount) { range = sel.getRangeAt(0).cloneRange(); if (range.getClientRects) { range.collapse(true); if (range.getClientRects().length>0){ rect = range.getClientRects()[0]; x = rect.left; y = rect.top; } } // Fall back to inserting a temporary element if (x == 0 && y == 0) { var span = document.createElement("span"); if (span.getClientRects) { // Ensure span has dimensions and position by // adding a zero-width space character span.appendChild( document.createTextNode("\u200b") ); range.insertNode(span); rect = span.getClientRects()[0]; x = rect.left; y = rect.top; var spanParent = span.parentNode; spanParent.removeChild(span); // Glue any broken text nodes back together spanParent.normalize(); } } } } return { x: x, y: y }; } window.ARPABET_SYMBOLS_v2 = [ "AA0", "AA1", "AA2", "AE0", "AE1", "AE2", "AH0", "AH1", "AH2", "AO0", "AO1","AO2", "AW0", "AW1", "AW2", "AY0", "AY1", "AY2", "B", "CH", "D", "DH", "EH0", "EH1", "EH2", "ER0", "ER1", "ER2", "EY0", "EY1", "EY2", "F", "G", "HH", "IH0", "IH1", "IH2", "IY0", "IY1", "IY2", "JH", "K", "L", "M", "N", "NG", "OW0", "OW1", "OW2", "OY0", "OY1", "OY2", "P", "R", "S", "SH", "T", "TH", "UH0", "UH1", "UH2", "UW0", "UW1", "UW2", "V", "W", "Y", "Z", "ZH" ] let ARPABET_SYMBOLS = [ 'AA0', 'AA1', 'AA2', 'AA', 'AE0', 'AE1', 'AE2', 'AE', 'AH0', 'AH1', 'AH2', 'AH', 'AO0', 'AO1', 'AO2', 'AO', 'AW0', 'AW1', 'AW2', 'AW', 'AY0', 'AY1', 'AY2', 'AY', 'B', 'CH', 'D', 'DH', 'EH0', 'EH1', 'EH2', 'EH', 'ER0', 'ER1', 'ER2', 'ER', 'EY0', 'EY1', 'EY2', 'EY', 'F', 'G', 'HH', 'IH0', 'IH1', 'IH2', 'IH', 'IY0', 'IY1', 'IY2', 'IY', 'JH', 'K', 'L', 'M', 'N', 'NG', 'OW0', 'OW1', 'OW2', 'OW', 'OY0', 'OY1', 'OY2', 'OY', 'P', 'R', 'S', 'SH', 'T', 'TH', 'UH0', 'UH1', 'UH2', 'UH', 'UW0', 'UW1', 'UW2', 'UW', 'V', 'W', 'Y', 'Z', 'ZH' ] let extra_arpabet_symbols = [ "AX", "AXR", "IX", "UX", "DX", "EL", "EM", "EN0", "EN1", "EN2", "EN", "NX", "Q", "WH", ] let new_arpabet_symbols = [ "RRR", "HR", "OE", "RH", "TS", "RR", "UU", "OO", "KH", "SJ", "HJ", "BR", ] window.ARPABET_SYMBOLS_v3 = ARPABET_SYMBOLS.concat(extra_arpabet_symbols).concat(new_arpabet_symbols).sort((a,b)=>a { const defaultLang = "en" let text = dialogueInput.value const finishedParts = [] // const parts = text.split(/\\lang\{[a-z]{0,2}\}\{/gi) const parts = text.split(/\\lang\[[a-z]{0,2}\]\[/gi) // const matchIterator = text.matchAll(/\\lang\{[a-z]{0,2}\}\{/gi) const matchIterator = text.matchAll(/\\lang\[[a-z]{0,2}\]\[/gi) finishedParts.push([defaultLang, parts[0]]) const langStack = [] // console.log("parts", parts) let textCounter = parts[0].length let caretInARPAbet = false parts.forEach((part,pi) => { if (pi) { const match = matchIterator.next().value[0] const langCode = match.split("lang[")[1].split("]")[0] langStack.push(langCode) // console.log("add langStack", langStack) textCounter += match.length let unescaped_part = "" part.split("]").forEach(sub_part => { // console.log("sub_part", sub_part, textCounter, textCounter+sub_part.length) if (caretStart > textCounter && caretStart to open the auto-complete } // if (sub_part.includes("[")) { // unescaped_part += sub_part+"]" // return // } sub_part = unescaped_part+sub_part unescaped_part = "" if (part.includes("]")) { finishedParts.push([langStack.pop()||defaultLang, sub_part]) } else { finishedParts.push([langStack[langStack.length-1], sub_part]) } // finishedParts.push([langStack.pop()||defaultLang, sub_part]) }) } }) return caretInARPAbet } window.hideAutocomplete = () => { textEditorTooltip.style.display = "none" textEditorTooltip.innerHTML = "" autocomplete_callback = undefined } let textWrittenSinceAutocompleteWasShown = "" const filterOrHideAutocomplete = () => { if (textEditorTooltip.style.display=="flex") { highlightedAutocompleteIndex = 0 let childrenShown = 0 Array.from(textEditorTooltip.children).forEach(child => { if (child.classList.contains("autocomplete_option_active")) { child.classList.toggle("autocomplete_option_active") } if (child.innerText.toLowerCase().startsWith(textWrittenSinceAutocompleteWasShown) || textWrittenSinceAutocompleteWasShown.length==0) { child.style.display = "flex" childrenShown += 1 } else { child.style.display = "none" } }) if (childrenShown==0) { hideAutocomplete() return } setHighlightedAutocomplete(0, true) } } let autocomplete_callback = undefined const showAutocomplete = (options, callback) => { const position = getCaretPosition(dialogueInput) // The getCaretPosition function doesn't work anymore. At least center it position.x = window.visualViewport.width/2 textEditorTooltip.style.left = position.x + "px" textEditorTooltip.style.top = position.y + "px" highlightedAutocompleteIndex = 0 textWrittenSinceAutocompleteWasShown = "" autocomplete_callback = callback options.forEach(option => { const optElem = createElem("div.autocomplete_option", option[0]) optElem.dataset.autocomplete_return = option.length>1 ? option[1] : option[0] optElem.addEventListener("click", () => { callback(optElem.dataset.autocomplete_return) hideAutocomplete() refreshText() }) textEditorTooltip.appendChild(optElem) }) textEditorTooltip.style.display = "flex" setHighlightedAutocomplete(0, true) return } let highlightedAutocompleteIndex = 0 const setHighlightedAutocomplete = (delta, override=false) => { let NEW_highlightedAutocompleteIndex = Math.min(textEditorTooltip.children.length-1, Math.max(0, highlightedAutocompleteIndex+delta)) if (override || NEW_highlightedAutocompleteIndex != highlightedAutocompleteIndex) { if (!override) { textEditorTooltip.children[highlightedAutocompleteIndex].classList.toggle("autocomplete_option_active") } textEditorTooltip.children[override ? delta : NEW_highlightedAutocompleteIndex].classList.toggle("autocomplete_option_active") highlightedAutocompleteIndex = NEW_highlightedAutocompleteIndex // textEditorTooltip.children[highlightedAutocompleteIndex].scrollIntoView() } } dialogueInput.addEventListener("keydown", event => { if (event.key=="Tab" || event.key=="Enter") { event.preventDefault() if (autocomplete_callback!==undefined) { autocomplete_callback(textEditorTooltip.children[highlightedAutocompleteIndex].dataset.autocomplete_return) hideAutocomplete() refreshText() return } let cursorIndex = dialogueInput.selectionStart if (cursorIndex) { // Move into the next [] bracket if already wrote down language let textPreCursor = dialogueInput.value.slice(0, cursorIndex) let textPostCursor = dialogueInput.value.slice(cursorIndex, dialogueInput.value.length) if (textPreCursor.slice(textPreCursor.length-8, 7)=="\\lang[") { dialogueInput.setSelectionRange(cursorIndex+2,cursorIndex+2) // Move out of [] if at the end } else if (textEditorTooltip.style.display=="none" && (textPostCursor.startsWith("]") || textPostCursor.startsWith("}"))) { console.log("moving one") dialogueInput.setSelectionRange(cursorIndex+1,cursorIndex+1) } } } }) const splitWords = (sequence, addSpace) => { const words = [] // const sequenceProcessed = sequence const sequenceProcessed = [] // Do further processing to also split on { } symbols, not just spaces sequence.forEach(word => { if (word.includes("{")) { word.split("{").forEach((w, wi) => { sequenceProcessed.push(wi ? ["{"+w, addSpace] : [w, false]) }) } else if (word.includes("}")) { word.split("}").forEach((w, wi) => { sequenceProcessed.push(wi ? [w, addSpace] : [w+"}", false]) }) } else { sequenceProcessed.push([word, addSpace]) } }) sequenceProcessed.forEach(([word, addSpace]) => { if (word.startsWith("\\lang[")) { words.push(word.split("][")[0]+"][") word = word.split("][")[1] } ["}","]","[","{"].forEach(char => { if (word.startsWith(char)) { words.push(char) word = word.slice(1,word.length) } }) const endExtras = []; ["}","]","[","{"].forEach(char => { if (word.endsWith(char)) { endExtras.push(char) word = word.slice(0,word.length-1) } }) words.push(word) endExtras.reverse().forEach(extra => words.push(extra)) // if (word.startsWith("{")) { // split_words.push("{") // word = word.slice(1,word.length) // } // if (word.endsWith("}")) { // split_words.push("{") // word = word.slice(1,word.length) // } if (addSpace) { words.push(" ") } }) return words } window.refreshText = () => { let all_text = dialogueInput.value textEditorElem.innerHTML = "" let openedCurlys = 0 let openedLangs = 0 let split_words = splitWords(all_text.split(" "), true) split_words = splitWords(split_words) split_words = splitWords(split_words) split_words = splitWords(split_words) // console.log("split_words", split_words) // dfgd() let caretCounter = 0 let caretInARPAbet = false let firstOpenCurly = undefined let lastOpenCurly = undefined split_words.forEach(word => { if (caretCounter<=dialogueInput.selectionStart && (caretCounter+word.length)>dialogueInput.selectionStart) { // console.log(`caret (${dialogueInput.selectionStart}) in counter (${caretCounter}): `, word, openedCurlys, openedLangs) caretInARPAbet = openedCurlys > 0 } caretCounter += word.length const spanElem = createElem("span.manyWhitespace", word) if (word.startsWith("\\lang[")) { openedLangs += 1 } if (word.startsWith("{")) { openedCurlys += 1 if (!caretInARPAbet) { firstOpenCurly = spanElem } } if (openedCurlys) { spanElem.style.fontWeight = "bold" spanElem.style.fontStyle = "italic" } if (openedLangs) { spanElem.style.background = "rgba(50, 150, 250, 0.2)" } ///==== if (word.includes("part-highlighted")) { spanElem.style.textDecoration = "underline dotted red" } else if (word.includes("highlighted")) { spanElem.style.textDecoration = "underline solid red" } ///==== if (word.endsWith("]")) { openedLangs -= 1 } if (word.endsWith("}")) { openedCurlys -= 1 if (caretInARPAbet && lastOpenCurly===undefined) { lastOpenCurly = spanElem } } textEditorElem.appendChild(spanElem) }) preprocess(dialogueInput.selectionStart) return [caretInARPAbet, firstOpenCurly, lastOpenCurly] } const languagesList = Object.keys(window.supportedLanguages) const insertText = (inputTextArea, textToInsert, caretOffset=0) => { let cursorIndex = inputTextArea.selectionStart inputTextArea.value = inputTextArea.value.slice(0, cursorIndex) + textToInsert + inputTextArea.value.slice(cursorIndex, inputTextArea.value.length) caretOffset += textToInsert.length inputTextArea.setSelectionRange(cursorIndex+caretOffset,cursorIndex+caretOffset) refreshText() } dialogueInput.addEventListener("keydown", event => { generateVoiceButton.disabled = window.currentModel==undefined || !dialogueInput.value.length if (event.key=="Enter") { event.stopPropagation() event.preventDefault() return } if (textEditorTooltip.style.display=="flex" && (event.key=="ArrowDown" || event.key=="ArrowUp" || (!window.shiftKeyIsPressed && event.key=="ArrowLeft") || (!window.shiftKeyIsPressed && event.key=="ArrowRight"))) { // if (textEditorTooltip.style.display=="flex" && (event.key=="ArrowDown" || event.key=="ArrowUp")) { event.stopPropagation() event.preventDefault() return } if (event.key=="}") { if (dialogueInput.value.slice(dialogueInput.selectionStart, dialogueInput.value.length-1).startsWith("}")) { dialogueInput.setSelectionRange(dialogueInput.selectionStart+1, dialogueInput.selectionStart+1) event.stopPropagation() event.preventDefault() return } } if (event.key=="]") { if (dialogueInput.value.slice(dialogueInput.selectionStart, dialogueInput.value.length-1).startsWith("]")) { dialogueInput.setSelectionRange(dialogueInput.selectionStart+1, dialogueInput.selectionStart+1) event.stopPropagation() event.preventDefault() return } } }) let is_doing_gp2 = false window.get_g2p = (text_to_g2p) => { return new Promise(resolve => { doFetch("http://localhost:8008/getG2P", {method: "Post", body: JSON.stringify({base_lang: base_lang_select.value, text: text_to_g2p})}) .then(r=>r.text()).then(res => { is_doing_gp2 = false resolve(res) }) }) } const handleTextUpdate = (event) => { generateVoiceButton.disabled = window.currentModel==undefined || !dialogueInput.value.length window.shiftKeyIsPressed = event.shiftKey if (textEditorTooltip.style.display=="flex" && (event.type=="click" || (!window.shiftKeyIsPressed && event.key=="ArrowDown") || (!window.shiftKeyIsPressed && event.key=="ArrowRight"))) { event.stopPropagation() event.preventDefault() setHighlightedAutocomplete(1) return } if (textEditorTooltip.style.display=="flex" && (event.type=="click" || (!window.shiftKeyIsPressed && event.key=="ArrowLeft") || (!window.shiftKeyIsPressed && event.key=="ArrowUp"))) { event.stopPropagation() event.preventDefault() setHighlightedAutocomplete(-1) return } if (event.type!="click" && (event.key=="Shift" || event.key=="Control")) { event.stopPropagation() event.preventDefault() return } const [caretInARPAbet, firstOpenCurly, lastOpenCurly] = refreshText() if (caretInARPAbet) { firstOpenCurly && (firstOpenCurly.style.color = "red") lastOpenCurly && (lastOpenCurly.style.color = "red") } textEditorElem.scrollTop = dialogueInput.scrollTop if (dialogueInput.selectionStart!=dialogueInput.selectionEnd && !is_doing_gp2) { hideAutocomplete() showAutocomplete([["<Convert to phonemes>"]], () => { const text_to_g2p = dialogueInput.value.slice(dialogueInput.selectionStart, dialogueInput.selectionEnd) is_doing_gp2 = true get_g2p(text_to_g2p).then(phonemes => { const initialStart = dialogueInput.selectionStart dialogueInput.value = dialogueInput.value.slice(0, dialogueInput.selectionStart) + dialogueInput.value.slice(dialogueInput.selectionEnd, dialogueInput.value.length) dialogueInput.selectionStart = initialStart insertText(dialogueInput, phonemes, 0) }) }) } else // } else { if (event.type!="click" && event.key.length==1 && event.key.match(/[a-z]/i)) { textWrittenSinceAutocompleteWasShown += event.key.toLowerCase() filterOrHideAutocomplete() } else if (event.type!="click" && event.key=="Backspace") { if (textWrittenSinceAutocompleteWasShown.length==0) { hideAutocomplete() } else { textWrittenSinceAutocompleteWasShown = textWrittenSinceAutocompleteWasShown.slice(0,textWrittenSinceAutocompleteWasShown.length-1) filterOrHideAutocomplete() } } else { hideAutocomplete() } const ctrlSpace = event.ctrlKey && event.code=="Space" if (event.type!="click" && (event.key=="{" || event.key=="") || ctrlSpace) { if (!ctrlSpace && event.key!="") { insertText(dialogueInput, "}", -1) } if (caretInARPAbet) { let symbols = window.ARPABET_SYMBOLS_v3 if (window.currentModel&&window.currentModel.modelType=="FastPitch1.1") { symbols = window.ARPABET_SYMBOLS_v2 } else if (window.currentModel&&window.currentModel.modelType=="FastPitch") { symbols = ["<ARPAbet only available for v2+ models>"] } showAutocomplete(symbols.map(v=>{return [v]}), option => { if (symbols.length>1) { insertText(dialogueInput, option.slice(textWrittenSinceAutocompleteWasShown.length, option.length)+" ", 0) } }) } if (event.key!="") { handleTextUpdate({type: "keydown", key: ""}) } } if (event.type!="click" && event.key=="\\") { // showAutocomplete([["\\lang[language][text]", "\\lang[][]"], ["\\sil[milliseconds]", "\\sil[]"]], (option) => { showAutocomplete([["\\lang[language][text]", "\\lang[][]"]], (option) => { if (option.includes("lang")) { insertText(dialogueInput, option.slice(1, option.length), -3) } else { insertText(dialogueInput, option.slice(1, option.length), -1) } setTimeout(() => { showAutocomplete(languagesList.map(v=>{return [v]}), option => { insertText(dialogueInput, option.slice(textWrittenSinceAutocompleteWasShown.length, option.length), 2) }) }, 100) }) } // } // }) } dialogueInput.addEventListener("click", event => handleTextUpdate(event)) dialogueInput.addEventListener("keyup", event => handleTextUpdate(event)) refreshText() setTimeout(window.refreshText, 500) window.addEventListener("click", event => { if (event.target && event.target!=textEditorTooltip && event.target.className && event.target.className.includes && !event.target.className.includes("autocomplete_option")) { hideAutocomplete() } })