中英文语音合成选读

调用responsivevoice.org的语音合成服务朗读选中文本

目前为 2015-12-17 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name pageSpeak
  3. // @name:zh-CN 中英文语音合成选读
  4. // @namespace tts@reverland.org
  5. // @description text to speech Service from responsivevoice.org to read aloud the page
  6. // @description:zh-CN 调用responsivevoice.org的语音合成服务朗读选中文本
  7. // @include *
  8. // @version 1.1
  9. // @grant GM_xmlhttpRequest
  10. // @grant GM_registerMenuCommand
  11. // ==/UserScript==
  12.  
  13. window.document.body.addEventListener("keyup", toggleTTS, true);
  14. //window.document.body.addEventListener("mouseup", tts, false);
  15.  
  16. GM_registerMenuCommand("start/stop tts", toggleTTS, "t");
  17. var toggle = false;
  18. var progressBar = document.createElement('progress');
  19. progressBar.style.position = "fixed";
  20. progressBar.style.left = "0";
  21. progressBar.style.bottom = "0";
  22. progressBar.style.display = "none";
  23. progressBar.style.zIndex = "99999";
  24. document.body.appendChild(progressBar);
  25. // 播放状态
  26. var playing = false;
  27.  
  28. function toggleTTS(e) {
  29. if (e && e.which == 69 && e.ctrlKey || !e) {//ctrl-e
  30. if (toggle) {
  31. window.document.body.removeEventListener("mouseup", tts, false);
  32. console.log("pageSpeak Stop...")
  33. toggle = false;
  34. } else {
  35. window.document.body.addEventListener("mouseup", tts, false);
  36. console.log("pageSpeak Start...")
  37. toggle = true;
  38. }
  39. }
  40. }
  41.  
  42. function tts(e) {
  43. var text = getText(e);
  44. if (!text) {
  45. return;
  46. }
  47. progressBar.style.display = "block";
  48. //console.log("text to speech: ", text);
  49. play(text);
  50. }
  51.  
  52.  
  53. function getText(e) {
  54. var selectObj = document.getSelection();
  55.  
  56. // if #text node
  57. if (selectObj.anchorNode.nodeType == 3) {
  58. //GM_log(selectObj.anchorNode.nodeType.toString());
  59. var word = selectObj.toString();
  60. if (word == "") {
  61. return;
  62. }
  63. // merge multiple spaces
  64. word = word.replace(/\s+/g, ' ');
  65. // linebreak wordwrap, optimize for pdf.js
  66. word = word.replace('-\n','');
  67. // multiline selection, optimize for pdf.js
  68. word = word.replace('\n', ' ');
  69. //console.log("word:", word);
  70. }
  71. return word
  72. }
  73.  
  74. // split by 100 length
  75. function *split(text, maxLength) {
  76. var index = 0;
  77. var regZh = /[\u4E00-\u9FD5]+/g
  78. var step = maxLength;
  79. if (!regZh.test(text)) {
  80. // en-US断句在标点边界
  81. var counter = 0;
  82. while (index < text.length && counter < 20) {
  83. step = maxLength;
  84. counter++;
  85. // search step;
  86. // dirty hack 10 is ok!
  87. for (let i =0; i < (((text.length - index) < 10)?(text.length - index):10); i++) {
  88. if (/^[\s.,?!]+$/m.test(text.substr(index + step, 1))) {
  89. step += 1
  90. break;
  91. } else {
  92. step -= 1;
  93. }
  94. if (step == 0) {
  95. // 包括最后一个字符
  96. step = maxLength + 1;
  97. break;
  98. }
  99. }
  100. yield text.slice(index, index + step);
  101. index += step;
  102. }
  103. } else {
  104. // chinese!
  105. var reg = /[a-zA-Z\s]+/g
  106. var counter = 0;
  107. while (index < text.length && counter < 20) {
  108. counter++;
  109. let result = reg.exec(text);
  110. if (result) {
  111. yield text.slice(index, result.index);
  112. yield result[0];
  113. step = result.index - index + result[0].length;
  114. } else {
  115. yield text.slice(index, index + maxLength);
  116. step = maxLength;
  117. }
  118. index += step;
  119. }
  120. }
  121. }
  122.  
  123. function play(text) {
  124. //console.log("[DEBUG] PLAYOUND")
  125. var context = new AudioContext();
  126. var voices = [];
  127. //var reg = /[\u4E00-\u9FD5]+/g
  128. var reg = /[\u4E00-\u9FCC]+/g
  129. for (let s of split(text, 100)) {
  130. if (!s) {
  131. return;
  132. }
  133. if (!reg.test(s)) {
  134. LANG = 'en-US';
  135. } else {
  136. LANG = 'zh-CN';
  137. }
  138. //var soundUrl = `https://code.responsivevoice.org/getvoice.php?t=${s}&tl=${LANG}&sv=&vn=&pitch=0.5&rate=0.5&vol=1`
  139. var soundUrl = `https://code.responsivevoice.org/develop/getvoice.php?t=${s}&tl=${LANG}&sv=&vn=&pitch=0.5&rate=0.5&vol=1`
  140. var p = new Promise(function(resolve, reject) {
  141. // console.log("text parts: ", s);
  142. var ret = GM_xmlhttpRequest({
  143. method: "GET",
  144. url: soundUrl,
  145. responseType: 'arraybuffer',
  146. onload: function(res) {
  147. try {
  148. // console.log("get data", res.statusText);
  149. resolve(res.response);
  150. progressBar.setAttribute('value', progressBar.getAttribute('value') + 1);
  151. } catch(e) {
  152. reject(e);
  153. }
  154. }
  155. });
  156. });
  157. voices.push(p);
  158. }
  159. progressBar.setAttribute('max', voices.length);
  160. progressBar.setAttribute('value', 0);
  161. Promise.all(voices).then(playSound, e=>console.log(e));
  162.  
  163. function playSound(bufferList) {
  164. // finish
  165. progressBar.style.display = "none";
  166. var reader = new FileReader();
  167. var blob = new Blob(bufferList, {type: 'application/octet-binary'});
  168. reader.addEventListener("loadend", function() {
  169. var buffer = reader.result;
  170. //console.log("final ArrayBuffer:", buffer);
  171. context.decodeAudioData(buffer, function(buffer) {
  172. if (playing) {
  173. console.log('playing: ', playing);
  174. try {
  175. source.stop();
  176. playing = false;
  177. } catch (e) {
  178. console.log(e);
  179. }
  180. }
  181. source = context.createBufferSource();
  182. source.buffer = buffer;
  183. source.connect(context.destination);
  184. source.start(0);
  185. playing = true;
  186. source.onended = () => {playing = false;}
  187. })
  188. });
  189. reader.readAsArrayBuffer(blob);
  190. }
  191. }