Auto Play Audio

使用脚本链接GPT-SoVITS。支持酒馆和www.perplexity.ai

当前为 2024-04-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Auto Play Audio
  3. // @namespace http://tampermonkey.net/
  4. // @version 7
  5. // @description 使用脚本链接GPT-SoVITS。支持酒馆和www.perplexity.ai
  6. // @author 从前跟你一样
  7. // @match *://*/*
  8. // @grant GM_xmlhttpRequest
  9. // @connet 192.168.10.2
  10. // ==/UserScript==
  11. (function() {
  12. 'use strict';
  13. let uRL="http://192.168.10.2:9880";//语音的ip地址。请一并修改上面的@connet 地址
  14. let PlayOther = true;// 是否播放旁白。是为true,否为false
  15. let timbre=true;// 是否区分音色。是为true,否为false
  16.  
  17. // 存储待播放的音频URL队列
  18. const audioQueue = [];
  19. // 存储待播放的音频队列
  20. const audios = [];
  21. // 是否有音频正在播放
  22. let isPlaying = false;
  23. //播放的序号
  24. let playnum = 0;
  25. //是否停止接收
  26. let iscont = true;
  27. //是否停止播放;
  28. let isstop = false;
  29. //正在获取语音
  30. let iswork=false;
  31. // 存储已经绑定过按钮的 div 元素
  32. const boundDivs = new Set();
  33. //用于存放当前播放的div
  34. let isPlayingDiv = "";
  35. //即将播放的div
  36. let nextPlayingDiv = "";
  37.  
  38. const statements = {
  39. 元气少女音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/元气少女音/香菱/闲聊/3.闲聊·觅食_嗯,闲着也是闲着,还不如一起找找食材去!.mp3&prompt_text=嗯,闲着也是闲着,还不如一起找找食材去!&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/xiangling-e15.pth&gweight=GPT_weights/xiangling_e8_s192.ckpt" ,
  40. 大叔音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/大叔音/钟离/闲聊/1.闲聊·旅程_旅程总有一天会迎来终点,不必匆忙。.mp3&prompt_text=旅程总有一天会迎来终点,不必匆忙。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/zhongli_e16_s432.pth&gweight=GPT_weights/zhongli-e30.ckpt",
  41. 萝莉音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/萝莉音/可莉/闲聊/1.闲聊·收获_可莉今天又勇敢地抓到了花纹奇怪的蜥蜴!从没见过这种图案,你要看看吗?.mp3&prompt_text=可莉今天又勇敢地抓到了花纹奇怪的蜥蜴!从没见过这种图案,你要看看吗?&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/keli_e8_s248.pth&gweight=GPT_weights/keli-e15.ckpt",
  42. 正太音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/正太音/班尼特/闲聊/3.闲聊·调侃_俗话说「等在原地是不会有好事发生的」,当然以我的运气来说,往哪里走都一样啦….mp3&prompt_text=俗话说「等在原地是不会有好事发生的」,当然以我的运气来说,往哪里走都一样啦…&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/bannite_e8_s200.pth&gweight=GPT_weights/bannite-e15.ckpt",
  43. 御姐音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/御姐音/八重神子/闲聊/1.闲聊·创作体裁_最近八重堂穿越异世的小说也太多了,哼,就对自己的世界如此不满吗。.mp3&prompt_text=最近八重堂穿越异世的小说也太多了,哼,就对自己的世界如此不满吗。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/bachongshenzi_e8_s256.pth&gweight=GPT_weights/bachongshenzi-e15.ckpt",
  44. 青年音二: "?text=你好&text_lang=中文&ref_audio_path=yuyin/青年音二/达达利亚/闲聊/0.初次见面…_我是愚人众执行官第十一席,「公子」达达利亚。而你——哈,也是能招致纷争之人,实在愉快。我们应该会很合得来吧?.mp3&prompt_text=我是愚人众执行官第十一席,「公子」达达利亚。而你——哈,也是能招致纷争之人,实在愉快。我们应该会很合得来吧?&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/dadaliya_e8_s200.pth&gweight=GPT_weights/dadaliya-e15.ckpt",
  45. // 旁白: "?text=你好&text_lang=中文&ref_audio_path=./smoke.wav&prompt_text=啊,是老公啊,啊&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/smoke_e8_s104.pth&gweight=GPT_weights/smoke-e15.ckpt",
  46. 旁白: "?text=你好&text_lang=中文&ref_audio_path=./smoke.wav&prompt_text=啊,是老公啊,啊&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/smoke_e4_s52.pth&gweight=GPT_weights/smoke-e15.ckpt",
  47. 青年混混音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/青年混混音/荒泷一斗/闲聊/41.收到赠礼·其二_好!你今天孝敬本大爷的,我都记在账上了。.mp3&prompt_text=好!你今天孝敬本大爷的,我都记在账上了。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/huanglongyidou_e8_s248.pth&gweight=GPT_weights/huanglongyidou-e15.ckpt",
  48. 青年音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/青年音/五郎/闲聊/3.闲聊·体格_魁梧的体格是力量的象征,对鼓舞士气也很有帮助。我得再加把劲啊!.mp3&prompt_text=魁梧的体格是力量的象征,对鼓舞士气也很有帮助。我得再加把劲啊!&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/wulang_e8_s208.pth&gweight=GPT_weights/wulang-e15.ckpt",
  49. 少萝音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/少萝音/迪奥娜/闲聊/47.突破的感受·承_猫的耳朵,听得见地上最细微的脚步声。.mp3&prompt_text=猫的耳朵,听得见地上最细微的脚步声。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/diaona_e8_s200.pth&gweight=GPT_weights/diaona-e15.ckpt",
  50. 少年音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/少年音/米卡/闲聊/5.打雷的时候…_雷声可以掩盖行动的声响,是我们的好朋友。.mp3&prompt_text=雷声可以掩盖行动的声响,是我们的好朋友。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/mika_e8_s264.pth&gweight=GPT_weights/mika-e15.ckpt",
  51. 少女御音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/少女御音/珐露珊/闲聊/9.早上好…_年轻人,早上就该拿出点精气神来!快重新向我问候一遍。.mp3&prompt_text=年轻人,早上就该拿出点精气神来!快重新向我问候一遍。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/falushan_e8_s240.pth&gweight=GPT_weights/falushan-e15.ckpt",
  52. 温柔少女音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/温柔少女音/神里绫华/闲聊/11.中午好…_午安。茶饭之后,难免略有困倦。是否有兴致下盘棋提神呢?.mp3&prompt_text=午安。茶饭之后,难免略有困倦。是否有兴致下盘棋提神呢?&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/shenlilinghua_e8_s224.pth&gweight=GPT_weights/shenlilinghua-e15.ckpt",
  53. 英气御姐音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/英气御姐音/迪希雅/闲聊/2.闲聊·沙漠_沙漠是个难相处的对手,但至少它足够光明磊落,把所有艰险挑战都放在台面上给你看。.mp3&prompt_text=沙漠是个难相处的对手,但至少它足够光明磊落,把所有艰险挑战都放在台面上给你看。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/dixiya_e8_s200.pth&gweight=GPT_weights/dixiya-e15.ckpt",
  54. 御中音: "?text=你好&text_lang=中文&ref_audio_path=yuyin/御中音/迪卢克/闲聊/0.初次见面…_蒙德城的迪卢克,应约而来。闲聊恕不奉陪,如果你是想做一番大事,我倒有点兴致。.mp3&prompt_text=蒙德城的迪卢克,应约而来。闲聊恕不奉陪,如果你是想做一番大事,我倒有点兴致。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/diluke2_e8_s240.pth&gweight=GPT_weights/diluke2-e15.ckpt",
  55. 御中音二: "?text=你好&text_lang=中文&ref_audio_path=yuyin/御中音二/赛诺/闲聊/6.打雷的时候…_在一些传说中,雷电是神明对人间降下的审判。.mp3&prompt_text=在一些传说中,雷电是神明对人间降下的审判。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/sainuo_e8_s208.pth&gweight=GPT_weights/sainuo-e15.ckpt",
  56. 御中音三: "?text=你好&text_lang=中文&ref_audio_path=yuyin/御中音三/神里绫人/闲聊/4.下雨的时候…_暂且先避下雨吧,不必着急,很快就会有人来送伞了。.mp3&prompt_text=暂且先避下雨吧,不必着急,很快就会有人来送伞了。&prompt_lang=中文&text_split_method=按标点符号切&sweight=SoVITS_weights/shenlilingren_e8_s224.pth&gweight=GPT_weights/shenlilingren-e15.ckpt",
  57. };
  58.  
  59.  
  60.  
  61.  
  62.  
  63. // 检测并插入播放按钮的函数
  64. function checkAndInsertPlayButton() {
  65. const divElements = document.querySelectorAll('div');
  66. divElements.forEach(div => {
  67. if (div.getAttribute('dir') === 'auto' && !boundDivs.has(div)||div.getAttribute('class') === 'mes_text'&& !boundDivs.has(div)) {
  68. const playButton = document.createElement('button');
  69. playButton.textContent = '播放';
  70. playButton.style.backgroundColor = '#4CAF50'; // 设置按钮背景颜色
  71. playButton.style.color = 'white'; // 设置按钮文字颜色
  72. playButton.style.padding = '8px 16px'; // 设置按钮内边距
  73. playButton.style.border = 'none'; // 移除按钮边框
  74. playButton.style.borderRadius = '4px'; // 设置按钮圆角
  75. playButton.style.cursor = 'pointer'; // 设置鼠标指针样式
  76. if(div.nextSibling.textContent=='播放'){
  77. div.nextSibling.remove();
  78. }
  79.  
  80. div.parentNode.insertBefore(playButton, div.nextSibling);
  81.  
  82. playButton.addEventListener('click',function(){
  83. if(this.textContent == '播放'){
  84. this.textContent = '停止';
  85. }else{
  86. this.textContent = '播放';
  87. }
  88. if(isPlayingDiv!=""&&isPlayingDiv!=div){
  89. isPlayingDiv.nextSibling.textContent = '播放';
  90. }
  91.  
  92.  
  93. if(isPlaying==false&&audios.length!=0&&isPlayingDiv==div){
  94. playnum=0;
  95. iswork=false;
  96. audioQueue.length = 0;
  97. audios.length=0;
  98. isstop=false;
  99.  
  100. }
  101. if (isPlaying ) {
  102. playnum=audios.length;
  103. isstop=true;
  104. }
  105.  
  106. nextPlayingDiv = div;
  107. });
  108.  
  109. boundDivs.add(div); // 将已经绑定过按钮的 div 元素添加到集合中
  110. }
  111. });
  112. }
  113.  
  114. // 每隔 5 秒检测一次 div 元素
  115. setInterval(checkAndInsertPlayButton, 5000);
  116.  
  117. //播放div中的音频
  118. async function playAudios(){
  119. if( iswork){
  120. return;
  121. }
  122. if(nextPlayingDiv==""){
  123. return;
  124. }
  125. if(nextPlayingDiv != isPlayingDiv){
  126. audioQueue.length = 0;
  127. audios.length=0;
  128. playnum = 0;
  129. isPlayingDiv=nextPlayingDiv;
  130. isstop=false;
  131. }
  132.  
  133. const div=isPlayingDiv;
  134. const text = div.textContent.trim();
  135. const quotes1 = extractElements(text);
  136. if(quotes1.length==audios.length){
  137. return;
  138. }
  139. let quotes2=[];
  140.  
  141. for (let i = audios.length; i <= quotes1.length-1; i++) {
  142. quotes2.push(quotes1[i]);
  143. audios.push(quotes1[i]);
  144. }
  145. for (const quote of quotes2) {
  146. if(isstop){
  147. iswork=false;
  148. return;
  149. }
  150. iswork=true;
  151. await fetchAudioAndAddToQueue(updateUrlWithText(uRL, quote),div);
  152. iswork=false;
  153. }
  154. };
  155. // 每隔 2秒播放语音
  156. setInterval(playAudios, 2000);
  157.  
  158. // 使用 GM_xmlhttpRequest 获取音频并添加到播放队列
  159. function fetchAudioAndAddToQueue(url,div) {
  160. if(isstop){
  161. iswork=false;
  162. return Promise.resolve();
  163. }
  164. return new Promise((resolve, reject) => {
  165. GM_xmlhttpRequest({
  166. method: "GET",
  167. url: url,
  168. responseType: 'blob',
  169. // signal: abortController.signal,
  170. onload: function(response) {
  171. if (response.status >= 200 && response.status < 300) {
  172. const audioUrl = window.URL.createObjectURL(response.response);
  173. audioQueue.push(audioUrl);
  174. if(isstop){
  175. iswork=false;
  176. return;
  177. }
  178. playNextAudio(div);
  179. resolve();
  180. } else {
  181. reject(new Error('Failed to load audio'));
  182. }
  183. },
  184. onerror: function() {
  185. reject(new Error('Network error'));
  186. }
  187. });
  188. });
  189. }
  190.  
  191. // 播放队列中的下一个音频
  192. function playNextAudio(div) {
  193. if(isstop){
  194. iswork=false;
  195. return;
  196. }
  197. if (isPlaying ) {
  198. return;
  199. }
  200. if ( audioQueue.length-1 < playnum ){
  201. div.nextSibling.textContent='播放';
  202. // alert("2");
  203. return;
  204. }
  205.  
  206. isPlaying = true;
  207. const audioUrl = audioQueue[playnum]; // 获取队列中的音频URL
  208. playnum=playnum+1;
  209. const audio = new Audio(audioUrl);
  210. audio.play();
  211. audio.onended = function() {
  212. isPlaying = false;
  213. playNextAudio(div); // 播放完毕后继续播放下一个
  214. };
  215. }
  216.  
  217.  
  218. function extractElements(text) {
  219. text=text.replace(/“|”/g, '"').replace(/(/g, '(').replace(/)/g, ')').replace(/\s*"\s*/g, '"');
  220.  
  221. //alert(text);
  222. let dialogueRegex= /(\(.+?\))"(.+?)"/g;
  223. const punctuationRegex = /[….,;:!?,。;:!?]/g;
  224. const elements = [];
  225.  
  226. let match;
  227. let lastIndex = 0;
  228. function splitNarration(narration, isLast = false) {
  229. const sentences = narration.split(punctuationRegex).map(sentence => sentence.trim()).filter(Boolean);
  230. if (isLast && !punctuationRegex.test(narration[narration.length - 1])) {
  231. sentences.pop();
  232. }
  233. return sentences;
  234. }
  235. //if((match = dialogueRegex.exec(text)) !== null){
  236. if(timbre){
  237. while ((match = dialogueRegex.exec(text)) !== null) {
  238.  
  239. const [_, voice, content] = match;
  240. const character = voice.slice(1, -1);
  241.  
  242. // 提取话语前的旁白
  243. const narration = text.slice(lastIndex, match.index).trim();
  244. if (narration&&PlayOther) {
  245. const sentences = splitNarration(narration);
  246. sentences.forEach(sentence => {
  247. elements.push(['旁白', sentence]);
  248. });
  249. }
  250.  
  251. // 提取话语
  252. const sentences2 = splitNarration(content);
  253. sentences2.forEach(sentence2 => {
  254. elements.push([character, sentence2]);
  255. });
  256.  
  257. lastIndex = dialogueRegex.lastIndex;
  258. }
  259.  
  260. // 提取最后一段旁白
  261. const finalNarration = text.slice(lastIndex).trim();
  262. const regex = /"/g;
  263. let matches =finalNarration.match(regex);
  264. let goon=false;
  265. if(matches){
  266. // alert(matches.length);
  267. goon=matches.length % 2 === 0;
  268. }
  269. if (finalNarration&&(!finalNarration.includes('"')||goon)&&PlayOther ){
  270. const sentences = splitNarration(finalNarration, true);
  271. if (sentences.length > 0) {
  272. const lastSentence = sentences[sentences.length - 1];
  273. sentences.forEach(sentence => {
  274. elements.push(['旁白', sentence]);
  275. });
  276. }
  277. };
  278. }else{ //不区分音色
  279. let dialogueRegex2= /"(.+?)"/g;
  280. while ((match = dialogueRegex2.exec(text)) !== null) {
  281. const [_, content] = match;
  282. const character = '旁白';
  283.  
  284. // 提取话语前的旁白
  285. const narration = text.slice(lastIndex, match.index).trim();
  286. if (narration&&PlayOther) {
  287. const sentences = splitNarration(narration);
  288. sentences.forEach(sentence => {
  289. elements.push(['旁白', sentence]);
  290. });
  291. }
  292.  
  293. // 提取话语
  294. const sentences2 = splitNarration(content);
  295. sentences2.forEach(sentence2 => {
  296. elements.push([character, sentence2]);
  297. });
  298.  
  299. lastIndex = dialogueRegex2.lastIndex;
  300. }
  301.  
  302. // 提取最后一段旁白
  303. const finalNarration = text.slice(lastIndex).trim();
  304. const regex = /"/g;
  305. let matches =finalNarration.match(regex);
  306. let goon=false;
  307. if(matches){
  308. // alert(matches.length);
  309. goon=matches.length % 2 === 0;
  310. }
  311.  
  312. if (finalNarration&&(!finalNarration.includes('"')||goon)&&PlayOther ){
  313. const sentences = splitNarration(finalNarration, true);
  314. if (sentences.length > 0) {
  315. const lastSentence = sentences[sentences.length - 1];
  316. sentences.forEach(sentence => {
  317. elements.push(['旁白', sentence]);
  318. });
  319. }
  320. };
  321.  
  322. }
  323. if(elements.length<1){
  324. elements.push(['旁白', "没有找到对话哦"]);
  325. }
  326. return elements;
  327. }
  328.  
  329.  
  330.  
  331.  
  332.  
  333.  
  334.  
  335. // 更新 URL 中的 text 参数
  336. function updateUrlWithText(uRl, quote) {
  337. // alert(quote[0]);
  338. const speak=statements[quote[0]];
  339. if(speak==null){
  340. quote[0]="旁白"
  341. }
  342. const url=uRl+statements[quote[0]];
  343. const urlObj = new URL(url);
  344. urlObj.searchParams.set('text', quote[1]);
  345. return urlObj.toString();
  346. }
  347. })();