const host=document.getElementById('script-input'); const draft=element.querySelector('textarea'), viewport=element.querySelector('.reading-window'); const track=element.querySelector('.reading-track'), edit=element.querySelector('[data-edit]'); const hint=element.querySelector('#script-hint'), pages=element.querySelector('[data-pages]'); let take=null, reading=false, clock=0, page=0, pageCount=1, spans=[], lineHeight=35; const listeners=new AbortController(); const on=(node,event,fn)=>node.addEventListener(event,fn,{signal:listeners.signal}); draft.value=props.value || ''; function publish() { props.value=draft.value; host.dataset.draft=draft.value; } function showMode() { draft.hidden=reading; viewport.hidden=!reading; edit.hidden=!take; edit.textContent=reading?'Edit text':'View take'; pages.hidden=!reading || take?.alignment==='ready' || pageCount<2; hint.textContent=reading?(take.alignment==='ready'?'': 'Word timing unavailable'):'Write your text ยท '+draft.value.length+'/600'; if(reading && take.text!==draft.value) hint.textContent='Previous take'; } function layout() { if(!reading || !spans.length) return; track.style.transform='none'; lineHeight=parseFloat(getComputedStyle(viewport).lineHeight); spans.forEach(s=>s.line=Math.max(0,Math.round(s.node.offsetTop/lineHeight))); pageCount=Math.max(1,Math.ceil((Math.max(...spans.map(s=>s.line))+1)/2)); renderClock(); showMode(); } function renderClock() { if(!reading || !take) return; if(take.alignment==='ready') { let index=-1; for(let i=0;i=spans[i].start; spans[i].node.classList.toggle('spoken',spoken); if(spoken) index=i; } page=index<0?0:Math.floor(spans[index].line/2); } else spans.forEach(s=>s.node.classList.add('spoken')); page=Math.max(0,Math.min(pageCount-1,page)); track.style.transform=`translateY(${-page*2*lineHeight}px)`; element.querySelector('[data-previous]').disabled=page===0; element.querySelector('[data-next]').disabled=page===pageCount-1; } function receive(data) { if(!data?.text) return; take=data; clock=0; page=0; track.replaceChildren(); const words=data.alignment==='ready' && data.words?.length?data.words: (data.text.match(/\S+\s*|\s+/gu)||[data.text]).map(text=>({text,start:0,end:0})); spans=words.map(word=>{ const node=document.createElement('span'); node.className='word'; node.textContent=word.text; track.append(node); return {...word,node,line:0}; }); reading=true; showMode(); requestAnimationFrame(layout); } on(draft,'input',()=>{publish();showMode();}); on(edit,'click',()=>{ reading=!reading; if(!reading) document.getElementById('speech-player')?.dispatchEvent(new CustomEvent('zonos:pause')); showMode(); if(reading) layout(); else draft.focus(); }); on(element.querySelector('[data-previous]'),'click',()=>{page--;renderClock();}); on(element.querySelector('[data-next]'),'click',()=>{page++;renderClock();}); on(host,'zonos:take',e=>receive(e.detail)); on(host,'zonos:clock',e=>{ clock=e.detail.time; if(e.detail.playing && !reading){reading=true;showMode();layout();} renderClock(); }); watch('value',()=>{if(props.value!==draft.value){draft.value=props.value||'';reading=false;showMode();}host.dataset.draft=draft.value;}); // Layout writes can resize Gradio's outer wrapper. Defer them outside the // observer delivery cycle to avoid a ResizeObserver feedback warning in WebKit. let resizeFrame=0; const resize=new ResizeObserver(()=>{ if(resizeFrame)return; resizeFrame=requestAnimationFrame(()=>{resizeFrame=0;if(element.isConnected)layout();}); });resize.observe(viewport); const observer=new MutationObserver(()=>{if(!element.isConnected){listeners.abort();cancelAnimationFrame(resizeFrame);resize.disconnect();observer.disconnect();}}); observer.observe(document.documentElement,{childList:true,subtree:true}); publish();showMode(); host.dataset.ready='true';