/* Duplex text/PCM speech, one task per answer. Credentials stay at the voice service. */ (()=>{ 'use strict'; if(document.documentElement.classList.contains('content-reader'))return; const bar=document.querySelector('#chat-voice-controls');if(!bar)return; const toggle=bar.querySelector('#chat-voice-toggle'),pause=bar.querySelector('#chat-voice-pause'),replay=bar.querySelector('#chat-voice-replay'),status=bar.querySelector('#chat-voice-status'); const endpoint='wss://app.creativestar.cn/chatGPT/webapi/queryVoiceStream.ashx'; const RATE=24000,MAX_BYTES=16*1024*1024; let enabled=false,busy=false,final=false,latest='',committed='',cursor=0,blocked=false,paused=false,job=null,serial=0,phase='off',notice='',context=null,cache=null; try{enabled=localStorage.getItem('zhiwei-chat-voice')==='on'}catch{} function sync(next=phase,message=''){ phase=next;if(message)notice=message; const labels={off:'语音关',ready:'语音开',loading:'接收中',playing:'朗读中',paused:'已暂停',blocked:'待播放',ended:'语音开',error:'语音失败',revised:'待重播'}; bar.dataset.phase=phase;toggle.textContent=labels[phase]||'语音开';toggle.setAttribute('aria-pressed',String(enabled)); toggle.setAttribute('aria-label',enabled?'关闭回答语音朗读':'开启回答语音朗读');toggle.title=enabled?'关闭后停止朗读':'开启后边接收回答边朗读'; pause.disabled=!enabled||!job||['error','revised','ended'].includes(phase); pause.textContent=['paused','blocked'].includes(phase)?'▶':'Ⅱ';pause.setAttribute('aria-label',pause.textContent==='Ⅱ'?'暂停朗读':'继续朗读');pause.title=pause.getAttribute('aria-label'); replay.disabled=!enabled||busy||!final||!latest;replay.title='重新朗读完整回答';replay.setAttribute('aria-label',replay.title); status.textContent=notice||({loading:'正在接收语音',playing:'正在朗读回答',paused:'朗读已暂停',blocked:'点击继续朗读即可播放',ended:'朗读结束',error:'语音暂不可用,回答完成后可点击重播',revised:'文字已调整,回答完成后可重新朗读'}[phase]||''); window.dispatchEvent(new CustomEvent('zhiwei-chat-voice',{detail:{phase}})); } function ensureContext(){ if(!context||context.state==='closed'){ const Audio=window.AudioContext||window.webkitAudioContext;if(!Audio)throw new Error('Audio unsupported'); context=new Audio(); } // Called synchronously from submit/toggle/manual-play to unlock browser audio. const promise=context.resume();promise?.catch(()=>{if(job&&!paused)sync('blocked')}); return context; } function cancelTransport(){ const old=job;job=null;serial++; if(old){ clearTimeout(old.startTimer);clearTimeout(old.inputTimer);clearTimeout(old.outputTimer);clearTimeout(old.lifeTimer);clearInterval(old.tick); const ws=old.ws; if(ws){try{if(ws.readyState===1)ws.send('{"type":"cancel"}');if(ws.readyState<2)ws.close()}catch{}} for(const s of old.sources){try{s.onended=null;s.stop();s.disconnect()}catch{}} old.sources.clear(); } if(context&&context.state==='running')context.suspend().catch(()=>{}); } function resetSpeech(){cancelTransport();committed='';cursor=0;paused=false;notice=''} function fail(message='语音连接中断,回答完成后可点击重播',kind='error'){ blocked=true;cache=null;cancelTransport();sync(enabled?kind:'off',message); } function textForVoice(raw){ const text=window.ZhiweiChatMarkdown?.toSpeechText(raw)??String(raw||''); return text.replace(/https?:\/\/[^\s<>\u3000-\u303f\uff00-\uffef]+/gi,'').replace(/\n{3,}/g,'\n\n').trim(); } // Commit only complete phrases outside unfinished Markdown links, code and emphasis. // Previously submitted raw prefixes are immutable; a rewrite cancels instead of repeating. function stableCut(text,start,complete){ if(complete)return text.length; let best=start,bracket=0,paren=0,tick=0,fence='',em='',lineStart=start===0||text[start-1]==='\n'; for(let i=start;i=70; if(sentence||period||phrase||ch==='\n')best=i+1; } lineStart=ch==='\n'; } return best; } function active(j){return job===j&&j.id===serial&&enabled} function updatePlayback(j){ if(!active(j))return; if(paused){sync('paused');return} if(context?.state!=='running'){sync('blocked');return} if(j.sources.size&&context.currentTime>=j.firstStart){ if(phase!=='playing'){ notice='';sync('playing'); document.querySelectorAll('#right-windows video,#right-windows audio').forEach(m=>m.pause()); document.querySelectorAll('.html-publication-frame').forEach(f=>f.contentWindow?.postMessage({type:'zhiwei-pause-publication-media'},'*')); } }else if(j.done&&!j.sources.size){ clearInterval(j.tick);notice='';sync('ended'); }else if(phase!=='loading'){notice='';sync('loading')} } function pcm(j,bytes,save=true){ if(!active(j)||!j.ready||j.done)throw new Error('Unexpected audio'); if(!(bytes instanceof ArrayBuffer)||bytes.byteLength>1048576)throw new Error('Invalid audio'); const data=new Uint8Array(bytes);j.bytes+=data.length; if(j.bytes>MAX_BYTES)throw new Error('Audio limit'); if(save)j.chunks.push(data.slice()); const combined=new Uint8Array(data.length+(j.odd===null?0:1));let offset=0; if(j.odd!==null){combined[0]=j.odd;offset=1} combined.set(data,offset);const count=Math.floor(combined.length/2);j.odd=combined.length%2?combined[combined.length-1]:null; if(!count)return; const ctx=context;if(!ctx||ctx.state==='closed')throw new Error('Audio closed'); const buffer=ctx.createBuffer(1,count,RATE),output=buffer.getChannelData(0),view=new DataView(combined.buffer); for(let i=0;i{source.disconnect();j.sources.delete(source);updatePlayback(j)}; source.start(at);updatePlayback(j); } function newJob(){ const j={id:++serial,ws:null,ready:false,endSent:false,done:false,pending:[],bytes:0,odd:null,chunks:[],sources:new Set(),firstStart:Infinity,nextTime:0}; job=j;j.tick=setInterval(()=>updatePlayback(j),80); j.lifeTimer=setTimeout(()=>{if(active(j))fail('语音会话超时,完成后可重播')},600000); return j; } function finishAudio(j){ if(!active(j))return; if(j.odd!==null||!j.bytes)throw new Error('Incomplete audio'); j.done=true;clearTimeout(j.startTimer);clearTimeout(j.inputTimer);clearTimeout(j.outputTimer);clearTimeout(j.lifeTimer); if(final&&cursor===latest.length)cache={text:latest,chunks:j.chunks}; updatePlayback(j); } function flush(j){ if(!active(j)||!j.ready||j.endSent)return; while(j.pending.length){ if(j.ws.readyState!==1||j.ws.bufferedAmount>1048576)throw new Error('Speech send failed'); j.ws.send(JSON.stringify({type:'text',text:j.pending.shift()})); } clearTimeout(j.inputTimer); if(final){ j.endSent=true;j.ws.send('{"type":"end"}'); j.outputTimer=setTimeout(()=>{if(active(j)&&!j.done)fail()},65000); }else j.inputTimer=setTimeout(()=>{if(active(j)&&!j.endSent)fail('文字输入暂停过久,回答完成后可点击重播')},18000); } function openSpeech(){ try{ensureContext()}catch{fail('浏览器暂不支持语音播放');return null} window.NewsBroadcast?.stop();const j=newJob();notice='';sync(paused?'paused':'loading'); try{ const ws=new WebSocket(endpoint);j.ws=ws;ws.binaryType='arraybuffer'; j.startTimer=setTimeout(()=>{if(active(j))fail('语音连接超时,回答完成后可点击重播')},30000); ws.addEventListener('open',()=>{ if(!active(j))return; ws.send(JSON.stringify({type:'start',model:'cosyvoice-v2',voice:'longxiaochun_v2',format:'pcm',sample_rate:RATE,volume:100,rate:1,pitch:1})); }); ws.addEventListener('message',event=>{ if(!active(j))return; try{ if(typeof event.data!=='string'){pcm(j,event.data);return} if(event.data.length>16384)throw new Error('Speech event limit'); const data=JSON.parse(event.data); if(data.type==='ready'){ if(j.ready||data.format!=='pcm'||data.sample_rate!==RATE||data.channels!==1||data.bits_per_sample!==16||data.voice!=='longxiaochun_v2'||data.model!=='cosyvoice-v2'||typeof data.task_id!=='string')throw new Error('Invalid ready'); j.ready=true;j.taskId=data.task_id;clearTimeout(j.startTimer);flush(j); }else if(data.type==='done'){ if(!j.endSent||data.task_id!==j.taskId||data.audio_bytes!==j.bytes)throw new Error('Invalid done'); finishAudio(j);ws.close(); }else throw new Error('Speech failed'); }catch{fail()} }); ws.addEventListener('error',()=>{if(active(j)&&!j.done)fail()}); ws.addEventListener('close',()=>{if(active(j)&&!j.done)fail()}); }catch{fail();return null} return j; } function feed(complete){ if(!enabled||blocked)return; if(cursor&&!latest.startsWith(committed)){fail('回答文字已调整,已停止旧朗读;完成后可点击重播','revised');return} const cut=stableCut(latest,cursor,complete); if(cut>cursor){ const raw=latest.slice(cursor,cut),text=textForVoice(raw); committed=latest.slice(0,cut);cursor=cut; if(text){ const j=job||openSpeech();if(!j)return; for(let i=0;i{ enabled=!enabled;try{localStorage.setItem('zhiwei-chat-voice',enabled?'on':'off')}catch{} resetSpeech();blocked=false; if(!enabled){sync('off');return} try{ensureContext()}catch{fail('浏览器暂不支持语音播放');return} sync('ready');if(latest){if(final)manualStart();else feed(false)} }); pause.addEventListener('click',()=>{ if(!job)return; if(!paused&&phase!=='blocked'){paused=true;context?.suspend().catch(()=>{});sync('paused')} else{paused=false;notice='';try{ensureContext();updatePlayback(job)}catch{sync('blocked')}} }); replay.addEventListener('click',manualStart); function otherMedia(){blocked=true;cancelTransport();notice='';sync(enabled?'ready':'off')} window.addEventListener('zhiwei-other-playback',otherMedia); document.addEventListener('play',e=>{if(e.target.matches?.('.news-broadcast-video,#right-windows video,#right-windows audio'))otherMedia()},true); window.addEventListener('message',e=>{if(e.data?.type==='zhiwei-publication-media-playing'&&[...document.querySelectorAll('.html-publication-frame')].some(f=>f.contentWindow===e.source))otherMedia()}); document.addEventListener('visibilitychange',()=>{if(document.hidden&&job&&!job.done){paused=true;context?.suspend().catch(()=>{});sync('paused')}else if(document.hidden&&job?.sources.size){paused=true;context?.suspend().catch(()=>{});sync('paused')}}); window.addEventListener('pagehide',()=>{cancelTransport();context?.close().catch(()=>{})}); window.ZhiweiChatVoice=Object.freeze({ begin(){resetSpeech();busy=true;final=false;latest='';cache=null;blocked=false;if(enabled){try{ensureContext()}catch{blocked=true}}sync(enabled?'ready':'off')}, snapshot(text){if(!busy)return;latest=String(text||'');feed(false)}, offer(answer){busy=false;final=true;latest=String(answer||'');feed(true);if(!job&&!blocked)sync(enabled?'ready':'off')}, fail(){cancelTransport();busy=false;final=false;latest='';cache=null;blocked=true;notice='';sync(enabled?'ready':'off','回答未完成,朗读已停止')}, endQuestion(){busy=false;sync()}, isActive:()=>!!job&&(!job.done||job.sources.size>0), stop:otherMedia }); sync(enabled?'ready':'off'); })();