text to speech Service from responsivevoice.org to read aloud the page
// ==UserScript== // @name pageSpeak // @name:zh-CN 中英文语音合成选读 // @namespace [email protected] // @description text to speech Service from responsivevoice.org to read aloud the page // @description:zh-CN 调用responsivevoice.org的语音合成服务朗读选中文本 // @include * // @version 1.1 // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // ==/UserScript== window.document.body.addEventListener("keyup", toggleTTS, true); //window.document.body.addEventListener("mouseup", tts, false); GM_registerMenuCommand("start/stop tts", toggleTTS, "t"); var toggle = false; var progressBar = document.createElement('progress'); progressBar.style.position = "fixed"; progressBar.style.left = "0"; progressBar.style.bottom = "0"; progressBar.style.display = "none"; progressBar.style.zIndex = "99999"; document.body.appendChild(progressBar); // 播放状态 var playing = false; function toggleTTS(e) { if (e && e.which == 69 && e.ctrlKey || !e) {//ctrl-e if (toggle) { window.document.body.removeEventListener("mouseup", tts, false); console.log("pageSpeak Stop...") toggle = false; } else { window.document.body.addEventListener("mouseup", tts, false); console.log("pageSpeak Start...") toggle = true; } } } function tts(e) { var text = getText(e); if (!text) { return; } progressBar.style.display = "block"; //console.log("text to speech: ", text); play(text); } function getText(e) { var selectObj = document.getSelection(); // if #text node if (selectObj.anchorNode.nodeType == 3) { //GM_log(selectObj.anchorNode.nodeType.toString()); var word = selectObj.toString(); if (word == "") { return; } // merge multiple spaces word = word.replace(/\s+/g, ' '); // linebreak wordwrap, optimize for pdf.js word = word.replace('-\n',''); // multiline selection, optimize for pdf.js word = word.replace('\n', ' '); //console.log("word:", word); } return word } // split by 100 length function *split(text, maxLength) { var index = 0; var regZh = /[\u4E00-\u9FD5]+/g var step = maxLength; if (!regZh.test(text)) { // en-US断句在标点边界 var counter = 0; while (index < text.length && counter < 20) { step = maxLength; counter++; // search step; // dirty hack 10 is ok! for (let i =0; i < (((text.length - index) < 10)?(text.length - index):10); i++) { if (/^[\s.,?!]+$/m.test(text.substr(index + step, 1))) { step += 1 break; } else { step -= 1; } if (step == 0) { // 包括最后一个字符 step = maxLength + 1; break; } } yield text.slice(index, index + step); index += step; } } else { // chinese! var reg = /[a-zA-Z\s]+/g var counter = 0; while (index < text.length && counter < 20) { counter++; let r###lt = reg.exec(text); if (r###lt) { yield text.slice(index, r###lt.index); yield r###lt[0]; step = r###lt.index - index + r###lt[0].length; } else { yield text.slice(index, index + maxLength); step = maxLength; } index += step; } } } function play(text) { //console.log("[DEBUG] PLAYOUND") var context = new AudioContext(); var voices = []; //var reg = /[\u4E00-\u9FD5]+/g var reg = /[\u4E00-\u9FCC]+/g for (let s of split(text, 100)) { if (!s) { return; } if (!reg.test(s)) { LANG = 'en-US'; } else { LANG = 'zh-CN'; } //var soundUrl = `https://code.responsivevoice.org/getvoice.php?t=${s}&tl=${LANG}&sv=&vn=&pitch=0.5&rate=0.5&vol=1` var soundUrl = `https://code.responsivevoice.org/develop/getvoice.php?t=${s}&tl=${LANG}&sv=&vn=&pitch=0.5&rate=0.5&vol=1` var p = new Promise(function(resolve, reject) { // console.log("text parts: ", s); var ret = GM_xmlhttpRequest({ method: "GET", url: soundUrl, responseType: 'arraybuffer', onload: function(res) { try { // console.log("get data", res.statusText); resolve(res.response); progressBar.setAttribute('value', progressBar.getAttribute('value') + 1); } catch(e) { reject(e); } } }); }); voices.push(p); } progressBar.setAttribute('max', voices.length); progressBar.setAttribute('value', 0); Promise.all(voices).then(playSound, e=>console.log(e)); function playSound(bufferList) { // finish progressBar.style.display = "none"; var reader = new FileReader(); var blob = new Blob(bufferList, {type: 'application/octet-binary'}); reader.addEventListener("loadend", function() { var buffer = reader.r###lt; //console.log("final ArrayBuffer:", buffer); context.decodeAudioData(buffer, function(buffer) { if (playing) { console.log('playing: ', playing); try { source.stop(); playing = false; } catch (e) { console.log(e); } } source = context.createBufferSource(); source.buffer = buffer; source.connect(context.destination); source.start(0); playing = true; source.onended = () => {playing = false;} }) }); reader.readAsArrayBuffer(blob); } }