🏠 Home 

pageSpeak

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);
}
}