const translation_token_generator = require('google-translate-token'); const replaceAll = require('string.prototype.replaceall'); const bodyParser = require('body-parser'); const express = require('express'); const request = require('request'); const cors = require('cors'); const fs = require('fs'); // if (process.platform == 'win32') { // const ioHook = require('iohook'); // ioHook.registerShortcut([96], (keys) => { // silence_timeout_exceeded_action(); // }); // ioHook.registerShortcut([110], (keys) => { // pause_toggle(); // }); // } var server = express(); server.use(cors()); server.use(express.urlencoded({ extended: true })); server.use(bodyParser.json({ extended: true })); const index_page = ` Tradutor simultâneo

Configurações

Idioma da fala



Idioma da 1ª tradução




Idioma da 2ª tradução




Visual

em
ex

Controle


Estatísticas
Traduções solicitadas:
Traduções recebidas:

Configure antes de usar


fala
trad1
trad2

(aguardando resposta) | Carregar de arquivo: | Salvar para arquivo
`.trim(); const main_style = ` body { font-family: sans-serif; background-color: black; color: white; } dl#transcriptions { display: flex; flex-wrap: wrap; } dl#transcriptions>dt { display: block; max-width: 4rem; flex: 0 0 4rem; border-right: 1px solid white; border-bottom: 1px solid white; } dl#transcriptions>dd { margin-inline-start: 4em; width: calc( 100% - 9rem ); flex: 0 0 calc( 100% - 9rem ); overflow-y: hidden; display: flex; flex-direction: column-reverse; font-weight: bold; } dl#transcriptions>dd>div { width: 100%; } dl#transcriptions>dt, dl#transcriptions>dd { align-items: baseline; margin: 1rem; margin-top: 0.5rem; } form { display: flex; flex-wrap: wrap; } .nospacing { margin: 0px; padding: 0px; } .formgroup { padding-left: 1rem; } .formgroup:not(:last-of-type) { border-right: 1px solid white; padding-right: 1rem; } form input[type=button], form input[type=text], form input[type=number], form textarea { background: black; color: white; border: white 0.08rem solid; border-radius: 0.2rem; padding: 0.2rem; padding-left: 0.4rem; padding-right: 0.4rem; } form input[type=text] { width: 6rem; } form input[type=number] { width: 3rem; } `.trim(); const main_script = ` window.SpeechRecognition = window.webkitSpeechRecognition || window.SpeechRecognition; LANGS_1 = [null, 'pt', 'en', 'es']; LANGS_2 = [null, 'pt-BR', 'en-US', 'es-AR']; SPOKEN_LANG_POS = 0; SELECTED_LANG1_POS = 1; SELECTED_LANG2_POS = 2; recognizing = false; recognition = new Object(); recognition.continuous = false; recognition.interimResults = true; recognition.lang = LANGS_2[SPOKEN_LANG_POS]; recog_id = generate_random_int_id(); from_lang = LANGS_1[SPOKEN_LANG_POS]; to_lang1 = LANGS_1[SELECTED_LANG1_POS]; to_lang2 = LANGS_1[SELECTED_LANG2_POS]; loader = document.querySelector('.loader'); listened = document.querySelector('.listened'); translated1 = document.querySelector('.translated1'); translated2 = document.querySelector('.translated2'); widthTestArea = document.querySelector('.widthTestArea'); cfgform = document.querySelector('#cfgform'); cfgformp = document.querySelector('#cfgformp'); trad_count = document.querySelector('#trad_count'); trans_count = document.querySelector('#trans_count'); link_baixar_substituicoes = document.querySelector('#link_baixar_substituicoes'); arquivo_de_substituicoes = document.querySelector('#arquivo_de_substituicoes'); adaptationsfield = document.querySelector('#adaptationsfield'); replacestatus = document.querySelector('#replacestatus'); bg = document.querySelector('html'); recognition.onstart = function() { recognizing = true; console.log('onstart'); } recognition.onerror = function(event) { console.log('onerror', event); aplicar(); } recognition.onend = function() { recognizing = false; console.log('onend'); aplicar(); } recognition.onsoundstart = function() { console.log('onsoundstart'); } recognition.onspeechstart = function() { console.log('onspeechstart'); } recognition.onresult = function(event) { var copied = new Object(); copied.lang = from_lang; copied.resultIndex = event.resultIndex; copied.identity = recog_id; copied.results = new Array(); for(var i = 0; i < event.results.length; i++) { speech_recognition_result = new Object(); speech_recognition_result.isFinal = event.results[i].isFinal; speech_recognition_result.alternatives = new Array(); for(var j = 0; j < event.results[i].length; j++) { speech_recognition_alternative = new Object(); speech_recognition_alternative.transcript = event.results[i][j].transcript; speech_recognition_alternative.confidence = event.results[i][j].confidence; speech_recognition_result.alternatives.push(speech_recognition_alternative); } copied.results.push(speech_recognition_result); } doXhr( 'POST', '/set/speech', ()=>{}, ()=>{}, JSON.stringify(copied), "application/json"); } function aplicar_conf() { newvalues = Object.fromEntries( Array.from( cfgform.querySelectorAll(':checked') ).map(x=>[x.name, parseInt(x.value)]) ); fontfamily = document.getElementById('ff').value; fontsize = '' + document.getElementById('fs').value + 'em'; boxheight = '' + document.getElementById('bh').value + 'ex'; textcolor = '' + document.getElementById('clr').value; SPOKEN_LANG_POS = newvalues['il'] || 0; SELECTED_LANG1_POS = newvalues['1l'] || 0; SELECTED_LANG2_POS = newvalues['2l'] || 0; from_lang = LANGS_1[SPOKEN_LANG_POS] || null; to_lang1 = LANGS_1[SELECTED_LANG1_POS] || null; to_lang2 = LANGS_1[SELECTED_LANG2_POS] || null; listened.style.fontFamily = fontfamily; translated1.style.fontFamily = fontfamily; translated2.style.fontFamily = fontfamily; widthTestArea.style.fontFamily = fontfamily; listened.style.fontSize = fontsize; translated1.style.fontSize = fontsize; translated2.style.fontSize = fontsize; widthTestArea.style.fontSize = fontsize; listened.style.height = boxheight; translated1.style.height = boxheight; translated2.style.height = boxheight; listened.style.color = textcolor; translated1.style.color = textcolor; translated2.style.color = textcolor; listened.style.display = 'flex'; translated1.style.display = 'flex'; translated2.style.display = 'flex'; listened.style.flexDirection = 'column-reverse'; translated1.style.flexDirection = 'column-reverse'; translated2.style.flexDirection = 'column-reverse'; if (from_lang === null) return; recognition_old = recognition; recognition = new SpeechRecognition(); recognition.lang = LANGS_2[SPOKEN_LANG_POS] || null; recognition.onstart = recognition_old.onstart; recognition.onerror = recognition_old.onerror; recognition.onend = recognition_old.onend; recognition.onresult = recognition_old.onresult; recognition.continuous = recognition_old.continuous; recognition.interimResults = recognition_old.interimResults; recognition.onsoundstart = recognition_old.onsoundstart; recognition.onspeechstart = recognition_old.onspeechstart; recognition_old.onend = null; recognition_old.onerror = null; recog_id = generate_random_int_id(); } function aplicar(){ loader.style.display = 'none'; aplicar_conf(); recognition.start(); doXhr( 'POST', '/set/config', ()=>{}, ()=>{}, JSON.stringify({ 'from_lang': from_lang, 'to_lang1': to_lang1, 'to_lang2': to_lang2, }), "application/json"); } function doXhr(method, URL, onsuccess, onfailure, body, bodyMime) { var xhr = new XMLHttpRequest(); xhr.open(method, URL); xhr.onload = function() { if (xhr.status != 200) onfailure(xhr); else onsuccess(xhr); }; if (bodyMime) xhr.setRequestHeader("Content-Type", bodyMime); xhr.onerror = () => onfailure(xhr); xhr.send(body); return xhr; } function generate_random_int_id() { return Math.trunc(Math.random()*Math.pow(2, 22)); } function check_translations() { setTimeout(check_translations, 100); doXhr( 'GET', '/get/speech', (xhr)=>{ var translations = JSON.parse(xhr.responseText); [listened, translated1, translated2].map((el, i) => { el.innerHTML = translations.boxes[i].split('\\n').map(escapeHtml).join('
'); measure_words(el); }); trad_count.innerText = translations.translation_count; trans_count.innerText = translations.translated_count; }, ()=>{}); } function measure_words(box){ wordlist = box.innerText.split(' ').filter(x=>x.length); wordlist = [...(new Set(wordlist.join(' ').split(' ').filter(x=>x.length)))]; wordoffsets = {}; widthTestArea.innerText = 'a a'; var a_a = widthTestArea.offsetWidth; widthTestArea.innerText = 'aa'; var aa = widthTestArea.offsetWidth; var _ = a_a - aa; wordoffsets[' '] = _; for (let word of wordlist) { widthTestArea.innerText = word; wordoffsets[word] = widthTestArea.offsetWidth; } widthTestArea.innerText = ''; measurement_summary = {}; measurement_summary.wordWidths = wordoffsets; measurement_summary.boxWidth = box.offsetWidth; doXhr( 'POST', '/set/measurements', ()=>{}, ()=>{}, JSON.stringify(measurement_summary), "application/json"); } function pausar() { doXhr( 'POST', '/set/togglepause', ()=>{}, ()=>{}); } function limpar() { doXhr( 'POST', '/set/clean', ()=>{}, ()=>{}); } var escapeHtml = (unsafe) => (unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'")); function carregar_substituicoes_de_arquivo() { var arq = arquivo_de_substituicoes.files[0]; arquivo_de_substituicoes.value = null; if(arq){ var fr = new FileReader(); fr.onload = ()=>{ var text = fr.result; adaptationsfield.value = text; novas_substituicoes(); } fr.readAsText(arq); } } function novas_substituicoes() { link_baixar_substituicoes.setAttribute('href', 'data:text/plain;plain,'+adaptationsfield.value); doXhr( 'POST', '/set/substituicoes', (xhr)=>{ replacestatus.innerText = xhr.responseText; }, ()=>{}, JSON.stringify({text: adaptationsfield.value}), "application/json"); } novas_substituicoes(); setTimeout(check_translations, 100); aplicar_conf(); `.trim(); var past_input_strings = []; var latest_input_id = null; var latest_input_string = ''; var assembled_input_string = ''; var out_strings = ['', '', '']; var out_paragraphs = {}; var lang_array = ['pt', null, null]; var last_translations = {}; const silence_timeout = 4000; var silence_timeout_id = null; const translation_timeout = 1500; var translator_locked = false; var translation_bounced_due_to_lock = false; const box_names = ['fala', 'trad1', 'trad2']; const lang_names = ['pt', 'en', 'es']; var translations_ordered = 0; var translations_received = 0; const translation_words_per_paragraph_max = 40; const max_past_input_strings = 5; var word_measurements = {}; var current_box_width = 0; var pausado = false; const measurement_error_factor = 1.2; const rgx_subst = /^\|(\w*)\|(.+?)((?:-|=){2})>(.*)$/; var substituicoes_textuais = []; function substituir_textualmente(texto, contextos) { let working = texto; for (let substituicao_textual of substituicoes_textuais) if (contextos.indexOf(substituicao_textual.ctx) >= 0) working = replaceAll( working, new RegExp( substituicao_textual.from, `g${substituicao_textual.cs ? '' : 'i'}`), substituicao_textual.to); return working; } function update_file(location, new_content) { return new Promise((resolve, reject) => { var errCallback = (err) => { if (err) reject(err); return !!err; }; var writeFileCallback = (err) => { if (!errCallback(err)) resolve(true); }; fs.exists(location, exists => { if (!exists) fs.writeFile( location, new_content, undefined, writeFileCallback); else { fs.readFile(location, (err, old_content) => { if (!errCallback(err)) { if (old_content == new_content) resolve(true); else fs.writeFile( location, new_content, undefined, writeFileCallback); } }); } }); }); } function do_translate(text, lang_from, lang_to) { return new Promise((resolve, reject) => { if (!text) return resolve(text); if (!lang_from) return resolve(text); if (!lang_to) return resolve(text); translation_token_generator.get(text).then((token) => { return request.post( 'https://translate.google.com/translate_a/single?' + 'client=webapp&' + `sl=${lang_from}&` + `tl=${lang_to}&` + `hl=${lang_from}&` + 'dt=at&' + 'dt=bd&' + 'dt=ex&' + 'dt=ld&' + 'dt=md&' + 'dt=qca&' + 'dt=rw&' + 'dt=rm&' + 'dt=ss&' + 'dt=t&' + 'otf=1&' + 'ssel=0&' + 'tsel=0&' + 'kc=1&' + `${token.name}=${token.value}`, { form: { q: text } }, (error, response, body) => { try { body = JSON.parse(body); } catch (e) { return reject([response.statusCode, response.statusMessage, response.request.uri.href]); } resolve(get_first_while_array(body)); } ); }, reject); }); } function try_to_translate() { out_strings[0] = substituir_textualmente( assembled_input_string, ['', box_names[0], lang_array[0]]); out_strings[0] = out_strings[0].split('\n').splice(-4).join('\n'); out_strings[0] = out_strings[0].split('\n').flatMap( paragraph => (paragraph.split(' ').length > translation_words_per_paragraph_max) ? split_into_lines(paragraph) : [paragraph] ).splice(-4).join('\n'); var translation_candidate = out_strings[0]; make_translations(translation_candidate); } function lock_translator_timedout() { translator_locked = false; if (translation_bounced_due_to_lock) { translation_bounced_due_to_lock = false; try_to_translate(); } } function lock_translator() { translator_locked = true; setTimeout(lock_translator_timedout, translation_timeout); } function prune_translations() { for (var k in last_translations) while (last_translations[k].length > 2048) last_translations[k].shift(); } function reset_silence_timeout() { if (silence_timeout_id !== null) clearTimeout(silence_timeout_id); silence_timeout_id = setTimeout(silence_timeout_exceeded_action, silence_timeout); } function silence_timeout_exceeded_action() { if (pausado) return reset_silence_timeout(); if (silence_timeout_id !== null) clearTimeout(silence_timeout_id); past_input_strings = []; latest_input_string = ''; out_strings[0] = ''; setTimeout(() => { out_strings[1] = ''; out_strings[2] = ''; }, 1150); } function make_translations(text) { prune_translations(); var translations_to_get = []; for (let i = 1; i < lang_array.length; i++) { if (lang_array[i] == null) out_strings[i] = ''; else if (lang_array[i] == lang_array[0]) out_strings[i] = out_strings[0]; else if (translations_to_get.indexOf(lang_array[i]) == -1) translations_to_get.push(lang_array[i]); } if (!translator_locked) { lock_translator(); let paragraphs = text.split('\n'); let this_translations = {}; for (let translang of translations_to_get) this_translations[translang] = paragraphs.map((x, i) => (out_paragraphs[translang] || [])[i] || '···' ); out_paragraphs = this_translations; for (let paragraph_index in paragraphs) { let paragraph = paragraphs[paragraph_index]; for (let translang of translations_to_get) { last_translations[translang] = last_translations[translang] || []; cached = Object.fromEntries(last_translations[translang])[paragraph]; if (cached) this_translations[translang][paragraph_index] = cached; else { translations_ordered += 1; do_translate(paragraph, lang_array[0], translang).then((translation) => { last_translations[translang].push([paragraph, translation]); this_translations[translang][paragraph_index] = translation; update_displayed_translations_for_lang(translang); translations_received += 1; }, err => console.log(`Erro ao traduzir para '${translang}': ${err}`)); } } } for (let i = 1; i < lang_array.length; i++) if (lang_array[i] == lang_array[0]) out_paragraphs[lang_array[i]] = paragraphs; let ps = paragraphs.length; if (ps >= 2) for (let translang of translations_to_get) if (this_translations[translang][ps - 2] === this_translations[translang][ps - 1]) this_translations[translang][ps - 1] = '···'; update_displayed_translations_all_langs(); } else translation_bounced_due_to_lock = true; } function update_displayed_translations_for_lang(translang) { for (let i = 1; i < lang_array.length; i++) if (lang_array[i] == translang) out_strings[i] = substituir_textualmente( out_paragraphs[translang].join('\n'), ['', lang_array[i], box_names[i]] ); } function update_displayed_translations_all_langs() { for (let i = 1; i < lang_array.length; i++) out_strings[i] = substituir_textualmente( (out_paragraphs[lang_array[i]] || []).join('\n'), ['', lang_array[i], box_names[i]] ); } function get_first_while_array(a) { if (Array.isArray(a)) return get_first_while_array(a[0]); else return a; } function pause_toggle() { pausado = !pausado; } server.get("/", (req, res) => { res.writeHead(200, "OK", { 'Content-Type': 'text/html' }); res.write(index_page); res.end(); }); server.get("/main.css", (req, res) => { res.writeHead(200, "OK", { 'Content-Type': 'text/css' }); res.write(main_style); res.end(); }); server.get("/main.js", (req, res) => { res.writeHead(200, "OK", { 'Content-Type': 'application/javascript' }); res.write(main_script); res.end(); }); server.post("/set/config", (req, res) => { lang_array[1] = req.body.to_lang1; lang_array[2] = req.body.to_lang2; res.end(); }); server.get("/get/speech", (req, res) => { res.writeHead(200, "OK", { 'Content-Type': 'application/json' }); res.write(JSON.stringify({ boxes: out_strings, translation_count: translations_ordered, translated_count: translations_received, })); res.end(); for (let i in lang_array) { update_file( 'live_translator_caixa_' + box_names[i] + '.txt', out_strings[i]); } for (let lang of [...(new Set(lang_array))]) { let i = lang_array.indexOf(lang); if (lang_array[i] === null) continue; update_file( 'live_translator_idioma_' + lang + '.txt', out_strings[i]); } for (let i in lang_array) { update_file( 'live_translator_3linhas_caixa_' + box_names[i] + '.txt', split_into_lines(out_strings[i]).splice(-3).join('\r\n')); } for (let i in lang_array) { update_file( 'live_translator_2linhas_caixa_' + box_names[i] + '.txt', split_into_lines(out_strings[i]).splice(-2).join('\r\n')); } for (let lang of [...(new Set(lang_array))]) { let i = lang_array.indexOf(lang); if (lang_array[i] === null) continue; update_file( 'live_translator_3linhas_idioma_' + lang + '.txt', split_into_lines(out_strings[i]).splice(-3).join('\r\n')); } for (let lang of [...(new Set(lang_array))]) { let i = lang_array.indexOf(lang); if (lang_array[i] === null) continue; update_file( 'live_translator_2linhas_idioma_' + lang + '.txt', split_into_lines(out_strings[i]).splice(-2).join('\r\n')); } }); server.post("/set/speech", (req, res) => { var speech = req.body; res.end(); if (!pausado) { if (speech.lang != lang_array[0]) { lang_array[0] = speech.lang; last_translations = {}; out_strings[0] = ''; assembled_input_string = ''; past_input_strings = []; latest_input_string = ''; latest_input_id = null; } if (speech.identity != latest_input_id) { latest_input_id = speech.identity; past_input_strings.push(latest_input_string); if (past_input_strings.length > max_past_input_strings) past_input_strings.shift(); latest_input_string = ''; } var s = ''; for (var i = speech.resultIndex; i < speech.results.length; i++) s += ' ' + speech.results[i].alternatives[0].transcript; s = s.split(' ').filter(x => x.length).join(' '); latest_input_string = s; reset_silence_timeout(); assembled_input_string = ( past_input_strings.join('\n') + '\n' + latest_input_string ).split('\n').map(paragraph => paragraph.split(' ').filter(x => x.length).join(' ') ).filter(x => x.length).join('\n'); try_to_translate(); } }); function split_into_lines(text) { var paragraphs = text.split('\n').map(p => p.split(' ').flatMap(x => [' ', x]).slice(1)); var lines = []; for (let tokens of paragraphs) { var line = []; for (let token of tokens) { if ((token == ' ') && (line.length == 0)) continue; let sizemapper = (wrd) => word_measurements[wrd] || 0; let tokensize = sizemapper(token); let linesize = line.map(sizemapper).reduce((a, b) => a + b, 0); if (linesize + tokensize > current_box_width / measurement_error_factor) { lines.push(line); line = []; } line.push(token); } lines.push(line); } for (let lnum in lines) lines[lnum] = lines[lnum].join('').trim(' '); lines = lines.filter(x => x.length); return lines; } server.post("/set/measurements", (req, res) => { var measurements = req.body; res.end(); for (let word in measurements.wordWidths) word_measurements[word] = measurements.wordWidths[word]; if (measurements.boxWidth) current_box_width = measurements.boxWidth; }); server.post("/set/togglepause", (req, res) => { res.end(); pause_toggle(); }); server.post("/set/clean", (req, res) => { res.end(); let op = pausado; pausado = false; silence_timeout_exceeded_action(); pausado = op; }); server.post("/set/substituicoes", (req, res) => { let substituicoes = req.body.text.trim().replace('\r\n', '\n').split('\n'); if (substituicoes.length == 1 && substituicoes[0] == '') substituicoes.pop(); let ctxs = ['', ...box_names, ...lang_names]; let cont_sucessos = 0; let cont_erros = 0; let erros_linhas = []; let sucessos = []; for (let i in substituicoes) { let linha = substituicoes[i]; let match = linha.match(rgx_subst); if (match != null && ctxs.indexOf(match[1]) >= 0) { let matched = { ctx: match[1], from: match[2], to: match[4], cs: match[3] == '==', }; // console.log(match); // console.log(matched); cont_sucessos += 1; sucessos.push(matched); } else { cont_erros += 1; erros_linhas.push(parseInt(i) + 1); } } res.write( `Sucessos: ${cont_sucessos}; ` + `Erros: ${cont_erros}` + ((cont_erros > 0) ? ( `, nas linhas [${erros_linhas.map(x => x.toString()).join(', ')}]` ) : '')); res.end(); substituicoes_textuais = sucessos; }); server.listen(13653, '127.0.0.1', () => { console.log('Abra http://localhost:13653 no Chrome.'); });