UncleanCode's picture
Dynamic Accent Duplex 0.2.0: Dynamic ASR real as default ASR (omniASR fallback)
ac573f1 verified
Raw History Blame
4.61 kB
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dynamic Accent Duplex (experimental)</title>
<style>body{font-family:system-ui,sans-serif;max-width:760px;margin:24px auto;padding:0 14px;color:#222}
button{font-size:16px;padding:10px 18px;margin-right:8px;border-radius:8px;border:1px solid #888;background:#f4f4f4;cursor:pointer}
#log{border:1px solid #ddd;border-radius:8px;padding:10px;height:420px;overflow:auto;font-size:14px;background:#fafafa}
.u{color:#0a4}.a{color:#036}.p{color:#999;font-style:italic}.m{color:#a60;font-size:12px}.s{color:#c00}
#dot{display:inline-block;width:12px;height:12px;border-radius:6px;background:#ccc;vertical-align:middle;margin-left:6px}
small{color:#666}</style></head><body>
<h2>Dynamic Accent Duplex <small>experimental cascaded voice agent</small></h2>
<p>Speak English, Nigerian Pidgin, Igbo, Yoruba or Hausa, mixed however you like. Ada answers in the same mix. You can interrupt her.
<b>Use headphones</b> (otherwise her voice can trigger barge-in).</p>
<button id="go">Start</button><button id="rs">Reset conversation</button> mic<span id="dot"></span>
<div id="log"></div>
<p><small>Pipeline: Silero VAD &rarr; Omnilingual ASR (CTC) &rarr; per-word language ID &rarr; Qwen3 LLM with mix validator &rarr; Dynamic Accent Flash TTS. Research demo; replies can be wrong, and Igbo/Yoruba/Hausa beyond simple phrases are weak.</small></p>
<script>
const log=(c,t)=>{const d=document.getElementById('log');const e=document.createElement('div');e.className=c;e.textContent=t;d.appendChild(e);d.scrollTop=1e9;return e};
const strip=t=>t.replace(/<\/?(eng|ibo|yor|hau|pcm)>/g,m=>m.startsWith('</')?'':'['+m.slice(1,-1)+'] ');
let ws,ctx,next=0,srcs=[],partialEl=null;
function play(buf){const i16=new Int16Array(buf);const f=new Float32Array(i16.length);for(let i=0;i<i16.length;i++)f[i]=i16[i]/32768;
const b=ctx.createBuffer(1,f.length,24000);b.copyToChannel(f,0);const s=ctx.createBufferSource();s.buffer=b;s.connect(ctx.destination);
const t=Math.max(ctx.currentTime+0.02,next);s.start(t);next=t+b.duration;srcs.push(s);s.onended=()=>{srcs=srcs.filter(x=>x!==s)}}
function stopAll(){srcs.forEach(s=>{try{s.stop()}catch(e){}});srcs=[];next=0}
document.getElementById('rs').onclick=()=>{ws&&ws.send(JSON.stringify({type:'reset'}));document.getElementById('log').innerHTML=''};
document.getElementById('go').onclick=async()=>{
ctx=new AudioContext({sampleRate:16000});
const st=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true,channelCount:1}});
const code=`class P extends AudioWorkletProcessor{constructor(){super();this.b=new Float32Array(512);this.n=0}
process(i){const x=i[0][0];if(!x)return true;for(let k=0;k<x.length;k++){this.b[this.n++]=x[k];if(this.n==512){const o=new Int16Array(512);
for(let j=0;j<512;j++){o[j]=Math.max(-32768,Math.min(32767,this.b[j]*32767))}this.port.postMessage(o.buffer,[o.buffer]);this.n=0}}return true}}
registerProcessor('p',P)`;
await ctx.audioWorklet.addModule(URL.createObjectURL(new Blob([code],{type:'application/javascript'})));
const node=new AudioWorkletNode(ctx,'p');ctx.createMediaStreamSource(st).connect(node);
ws=new WebSocket((location.protocol==='https:'?'wss':'ws')+'://'+location.host+location.pathname.replace(/\/$/,'')+'/ws');ws.binaryType='arraybuffer';
node.port.onmessage=e=>{if(ws.readyState===1)ws.send(e.data)};
ws.onopen=()=>log('m','connected - start talking');
ws.onmessage=e=>{if(typeof e.data!=='string'){play(e.data);return}const d=JSON.parse(e.data);
if(d.type==='vad'){document.getElementById('dot').style.background=d.speech?'#0a4':'#ccc'}
else if(d.type==='partial'){if(!partialEl)partialEl=log('p','');partialEl.textContent='… '+d.text}
else if(d.type==='final'){if(partialEl){partialEl.remove();partialEl=null}log('u','You: '+d.text)}
else if(d.type==='reply'){log('a','Ada: '+strip(d.text));log('m','target mix '+JSON.stringify(d.target.mix)+' '+(d.target.pattern||[]).join(',')+' | reply mix '+JSON.stringify(d.reply_mix)+' | '+d.how)}
else if(d.type==='metrics'){log('m','latency end-of-speech→first audio '+d.latency_s+' s (EOT wait '+d.eot_ms+' ms, ASR '+d.asr_ms+' ms, LLM '+d.llm_s+' s, TTS first chunk '+d.tts_first_s+' s)')}
else if(d.type==='stop'){stopAll();log('s','[barge-in: Ada stopped]')}
else if(d.type==='backchannel'){log('m','(backchannel)')}
else if(d.type==='info'){log('m',d.text)}};
ws.onclose=()=>log('s','disconnected');document.getElementById('go').disabled=true};
</script></body></html>