/* The editor stores original-image coordinates; canvas scaling never changes boxes. */ 'use strict'; const $ = (id) => document.getElementById(id); const state = {session:null, regions:[], selected:null, model:'meanflow', tool:'select', history:[], busy:false, source:null, result:null, stale:false, transport:'http'}; let gesture = null; const clone = (x) => structuredClone(x); const escape = (s) => String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const endpoint = (name) => `/api/sessions/${state.session.id}/${name}`; const exampleCache = new Map(); function prepareExample(example) { const cached = exampleCache.get(example); if(cached && cached.expires > Date.now()) return cached.promise; const entry = {expires:Date.now()+45*60*1000}; entry.promise = (async()=>{ const session = await api('/api/session',{example}); const image = new Image(); image.src=session.image; await image.decode(); return {session,image}; })(); exampleCache.set(example,entry); entry.promise.catch(()=>{if(exampleCache.get(example)===entry)exampleCache.delete(example);}); return entry.promise; } async function api(url, body, isForm=false) { const response = await fetch(url, body === undefined ? {} : {method:'POST', headers:isForm?{}:{'Content-Type':'application/json'}, body:isForm?body:JSON.stringify(body)}); const data = await response.json(); if (!response.ok) throw new Error(typeof data.detail === 'string' ? data.detail : 'Please check the image and region settings.'); return data; } function notice(message='', success=false) { $('notice').textContent=message; $('notice').hidden=!message; $('notice').classList.toggle('success',success); if(success)setTimeout(()=>{if($('notice').textContent===message)$('notice').hidden=true;},4500); } function pushHistory() { state.history.push(clone(state.regions)); if(state.history.length>30) state.history.shift(); $('undoButton').disabled=false; } function changed() { if(state.result) {state.stale=true; $('resultSummary').textContent='Regions or settings changed. Refine again to update.'; $('comparePanel').classList.add('result-stale');} updateControls(); } function selected() {return state.regions.find(r=>r.id===state.selected);} let activity = null, activityDismissTimer = null; function dismissActivity() { const returnFocus=activity?.status==='error'?$(activity.kind+'Button'):null; clearTimeout(activityDismissTimer);activityDismissTimer=null; $('jobProgress').hidden=true;activity=null;returnFocus?.focus(); } $('closeProgress').onclick=dismissActivity; function startActivity(kind, message) { clearTimeout(activityDismissTimer); activity={kind,status:'running'}; $('jobProgress').hidden=false;$('jobProgress').dataset.state='running'; $('jobTitle').textContent=kind==='detect'?'Detecting text':kind==='refine'?'Refining text':message; $('closeProgress').hidden=true; updateActivity(message); } function updateActivity(message, progress=null, waiting=true) { if(!activity||activity.status!=='running')return; const determinate=!waiting && typeof progress==='number' && Number.isFinite(progress); const percent=determinate?Math.round(Math.max(0,Math.min(1,progress))*100):null; if($('jobMessage').textContent!==message)$('jobMessage').textContent=message; $('jobMessage').hidden=message===$('jobTitle').textContent; $('jobProgress').classList.toggle('waiting',!determinate); $('jobPercent').hidden=!determinate; const label=determinate?`${percent}%`:''; if($('jobPercent').textContent!==label)$('jobPercent').textContent=label; $('progressFill').style.width=determinate?`${percent}%`:'0%'; $('progressTrack').setAttribute('aria-valuetext',message); if(determinate)$('progressTrack').setAttribute('aria-valuenow',String(percent)); else $('progressTrack').removeAttribute('aria-valuenow'); } function finishActivity(status, message) { if(!activity)return; if(status==='done')updateActivity(message,1,false); activity.status=status;$('jobProgress').dataset.state=status; $('jobTitle').textContent=status==='error'?'Something went wrong':activity.kind==='detect'?'Text detected':'Text refined'; $('jobMessage').textContent=message;$('closeProgress').hidden=status!=='error'; if(status==='error'){ $('jobProgress').classList.remove('waiting');$('jobPercent').hidden=true; $('progressTrack').removeAttribute('aria-valuenow'); $('closeProgress').focus(); }else{ const completed=activity; activityDismissTimer=setTimeout(()=>{if(activity===completed)dismissActivity();},1800); } } function setBusy(busy, message='Preparing', kind='loading') { state.busy=busy; document.querySelectorAll('[data-example],[data-model],#uploadButton,#exampleTargets,#resetButton,#drawTool,#undoButton,#targetText,#enableRegion,#deleteRegion,.coords input,.settings input,.region-row input').forEach(e=>e.disabled=busy); $('regionCanvas').style.pointerEvents=busy?'none':''; for(const name of ['detect','refine']){const active=busy&&kind===name;$(name+'Button').classList.toggle('is-running',active);$(name+'Button').setAttribute('aria-busy',String(active));} if(busy)startActivity(kind,message); else if(activity?.status==='running')dismissActivity(); updateControls(); } function updateControls() { const active=state.regions.filter(r=>r.enabled); $('selectedCount').textContent=`${active.length} selected`; $('selectedCount').classList.toggle('has-selected',active.length>0); $('boxCount').textContent=state.regions.length?`${state.regions.length} regions`:'No regions yet'; $('refineButton').disabled=state.busy || !active.length || active.length>24 || active.some(r=>!r.text.trim()); $('detectButton').disabled=state.busy || !state.session; $('undoButton').disabled=state.busy || !state.history.length; $('runNote').textContent=state.busy?'You can follow the progress above.':active.length?`${active.length} region${active.length>1?'s':''} · ${state.model==='meanflow'?'MeanFlow':'Teacher'} · ${$('steps').value} steps`:'Select a region and enter its target text.'; $('exampleTargets').hidden=!state.session?.example; } function switchView(view) { if(view==='compare'&&!state.result)return; $('editorPanel').hidden=view!=='edit'; $('comparePanel').hidden=view!=='compare'; ['edit','compare'].forEach(v=>{ $(v+'View').classList.toggle('active',v===view); $(v+'View').setAttribute('aria-selected',String(v===view)); }); } function applySettings(settings) { state.model=settings.variant||settings.model||state.model; document.querySelectorAll('[data-model]').forEach(button=>{ const active=button.dataset.model===state.model; button.classList.toggle('active',active);button.setAttribute('aria-checked',String(active)); }); for(const [key,id] of [['steps','steps'],['seed','seed'],['guidance','guidance'],['text_threshold','textThreshold']]){ if(settings[key]!=null)$(id).value=settings[key]; } } async function loadSession(session, source=null) { if(!source){source=new Image();source.src=session.image;await source.decode();} state.session=session;state.regions=clone(session.regions||[]);state.selected=state.regions[0]?.id||null;state.history=[];state.result=null;state.stale=false;state.tool='select'; if(session.settings)applySettings(session.settings); $('regionResults').hidden=true;$('compareView').disabled=true; $('regionCanvas').setAttribute('viewBox',`0 0 ${session.width} ${session.height}`); $('regionCanvas').style.aspectRatio=`${session.width} / ${session.height}`; const image=$('sourceImage'); image.setAttribute('href',session.image); image.setAttribute('width',session.width);image.setAttribute('height',session.height); $('imageSize').textContent=`${session.width} × ${session.height}`; $('wipe').style.aspectRatio=`${session.width} / ${session.height}`; state.source=source; document.querySelectorAll('[data-example]').forEach(e=>e.classList.toggle('active',e.dataset.example===session.example)); switchView('edit');setTool('select');render();notice(); } async function loadExample(example) { if(state.busy)return; try{ setBusy(true,'Loading example'); const prepared=await prepareExample(example); await loadSession(clone(prepared.session),prepared.image); history.replaceState(null,'',location.pathname); }catch(e){notice(e.message);}finally{setBusy(false);} } async function upload(file) { if(!file||state.busy)return; const form=new FormData();form.append('file',file); try{setBusy(true);await loadSession(await api('/api/upload',form,true));history.replaceState(null,'',location.pathname);}catch(e){notice(e.message);}finally{setBusy(false);$('imageUpload').value='';} } function renderBoxes() { if(!state.session)return; const scale=state.session.width/650; $('regionBoxes').innerHTML=state.regions.map((r,i)=>{ const [x,y,x1,y1]=r.bbox;const current=r.id===state.selected;const status=(r.enabled?' enabled':'')+(current?' selected':''); const size=8*scale, badgeW=23*scale, badgeH=18*scale; const labelY=Math.max(0,y-badgeH); const handles=current?[[x,y,'nw'],[x1,y,'ne'],[x,y1,'sw'],[x1,y1,'se']].map(([a,b,c])=>``).join(''):''; const label=(r.enabled||current)?`${i+1}`:''; return `${label}${handles}`; }).join(''); } function renderList() { $('regionsEmpty').hidden=!!state.regions.length; $('regionList').innerHTML=state.regions.map((r,i)=>`
${String(i+1).padStart(2,'0')}${escape(r.text||'Enter target text')}${r.origin==='example'?'example':r.origin==='manual'?'drawn':r.origin==='saved'?'saved':Math.round((r.confidence||0)*100)+'%'}
`).join(''); } function renderDetail() { const r=selected();$('regionDetail').hidden=!r;if(!r)return; $('targetText').value=r.text;$('enableRegion').checked=r.enabled; $('regionOrigin').textContent=r.origin==='ocr'?'OCR proposal':r.origin==='example'?'Example target':r.origin==='saved'?'Previous run':'Drawn by you'; const [x,y,x1,y1]=r.bbox; $('coordinates').innerHTML=[['X',x],['Y',y],['W',x1-x],['H',y1-y]].map(([label,value],index)=>``).join(''); const crop=$('selectedCrop'); const padding=Math.max(5,(y1-y)*.12); const left=Math.max(0,x-padding),top=Math.max(0,y-padding),width=Math.min(state.session.width,x1+padding)-left,height=Math.min(state.session.height,y1+padding)-top; crop.width=Math.max(1,Math.round(width));crop.height=Math.max(1,Math.round(height)); if(state.source?.complete)crop.getContext('2d').drawImage(state.source,left,top,width,height,0,0,crop.width,crop.height); } function render() {renderBoxes();renderList();renderDetail();updateControls();} function selectRegion(id, scroll=false) { state.selected=id;render(); if(scroll){const row=[...$('regionList').children].find(e=>e.dataset.id===id);row?.scrollIntoView({block:'nearest',behavior:'smooth'});} $('canvasHint').textContent=id?'Drag the box to move it. Drag a corner to resize it.':'Detect words to get started, or draw a box yourself.'; } function setTool(tool) { state.tool=tool;$('selectTool').classList.toggle('active',tool==='select');$('drawTool').classList.toggle('active',tool==='draw');$('regionCanvas').classList.toggle('drawing',tool==='draw'); $('drawHint').hidden=tool!=='draw'; $('canvasHint').textContent=tool==='draw'?'Drag over a word, then enter its target text.':'Click a box to select it. Drag a corner to resize.'; } function point(event) { const p=new DOMPoint(event.clientX,event.clientY).matrixTransform($('regionCanvas').getScreenCTM().inverse()); return [Math.max(0,Math.min(state.session.width,p.x)),Math.max(0,Math.min(state.session.height,p.y))]; } $('regionCanvas').addEventListener('pointerdown',event=>{ if(state.busy||!state.session||event.button!==0)return; event.preventDefault();const p=point(event),id=event.target.dataset.id; if(state.tool==='draw') {pushHistory();gesture={mode:'draw',start:p};$('draftBox').removeAttribute('hidden');$('draftBox').setAttribute('width',0);$('draftBox').setAttribute('height',0);} else if(id){selectRegion(id,true);pushHistory();gesture={mode:event.target.dataset.handle||'move',start:p,box:[...selected().bbox],id};} else{selectRegion(null);return;} $('regionCanvas').setPointerCapture(event.pointerId); }); $('regionCanvas').addEventListener('pointermove',event=>{ if(!gesture)return;const p=point(event),g=gesture; if(g.mode==='draw'){ const box=[Math.min(g.start[0],p[0]),Math.min(g.start[1],p[1]),Math.max(g.start[0],p[0]),Math.max(g.start[1],p[1])]; const draft=$('draftBox');draft.setAttribute('x',box[0]);draft.setAttribute('y',box[1]);draft.setAttribute('width',box[2]-box[0]);draft.setAttribute('height',box[3]-box[1]);g.box=box; } else{ const r=state.regions.find(r=>r.id===g.id),[x,y,x1,y1]=g.box;let box; if(g.mode==='move') {const dx=Math.max(-x,Math.min(state.session.width-x1,p[0]-g.start[0])),dy=Math.max(-y,Math.min(state.session.height-y1,p[1]-g.start[1]));box=[x+dx,y+dy,x1+dx,y1+dy];} else box=[g.mode.includes('w')?Math.min(p[0],x1-4):x,g.mode.includes('n')?Math.min(p[1],y1-4):y,g.mode.includes('e')?Math.max(p[0],x+4):x1,g.mode.includes('s')?Math.max(p[1],y+4):y1]; r.bbox=box.map(n=>Math.round(n*10)/10);renderBoxes();renderDetail(); } }); function endGesture(){ if(!gesture)return;const g=gesture;gesture=null;$('draftBox').setAttribute('hidden',''); if(g.mode==='draw'&&g.box&&g.box[2]-g.box[0]>=6&&g.box[3]-g.box[1]>=6){ const r={id:'manual-'+crypto.randomUUID(),bbox:g.box.map(n=>Math.round(n*10)/10),text:'',enabled:true,origin:'manual'}; state.regions.push(r);state.selected=r.id;setTool('select');render();$('targetText').focus(); }else render();changed(); } $('regionCanvas').addEventListener('pointerup',endGesture); $('regionCanvas').addEventListener('pointercancel',endGesture); $('regionList').addEventListener('click',event=>{if(event.target.tagName==='INPUT')return;const row=event.target.closest('[data-id]');if(row)selectRegion(row.dataset.id);}); $('regionList').addEventListener('keydown',event=>{if(event.key==='Enter'){const row=event.target.closest('[data-id]');if(row)selectRegion(row.dataset.id);}}); $('regionList').addEventListener('change',event=>{if(state.busy)return;const row=event.target.closest('[data-id]');if(!row)return;pushHistory();state.regions.find(r=>r.id===row.dataset.id).enabled=event.target.checked;selectRegion(row.dataset.id);changed();}); $('targetText').addEventListener('focus',()=>{if(selected())pushHistory();}); $('targetText').addEventListener('input',()=>{const r=selected();if(!r||state.busy)return;r.text=$('targetText').value;r.enabled=true;$('enableRegion').checked=true;renderList();renderBoxes();changed();}); $('enableRegion').addEventListener('change',()=>{if(!selected()||state.busy)return;pushHistory();selected().enabled=$('enableRegion').checked;render();changed();}); $('coordinates').addEventListener('change',event=>{ if(!selected()||state.busy||!event.target.matches('input'))return; const values=[...$('coordinates').querySelectorAll('input')].map(e=>Number(e.value));if(values.some(v=>!Number.isFinite(v)))return; pushHistory();const [a,b,w,h]=values,x=Math.max(0,Math.min(state.session.width-1,a)),y=Math.max(0,Math.min(state.session.height-1,b));selected().bbox=[x,y,Math.min(state.session.width,x+Math.max(1,w)),Math.min(state.session.height,y+Math.max(1,h))];render();changed(); }); function deleteRegion(){if(!selected()||state.busy)return;pushHistory();state.regions=state.regions.filter(r=>r.id!==state.selected);state.selected=null;render();changed();} $('deleteRegion').onclick=deleteRegion; $('undoButton').onclick=()=>{if(state.busy||!state.history.length)return;state.regions=state.history.pop();if(!selected())state.selected=null;render();changed();}; $('resetButton').onclick=()=>{if(state.busy||!state.regions.length)return;pushHistory();state.regions=[];state.selected=null;render();changed();}; $('selectTool').onclick=()=>setTool('select');$('drawTool').onclick=()=>setTool(state.tool==='draw'?'select':'draw'); document.addEventListener('keydown',event=>{if(event.key==='Escape'&&activity?.status==='error'){dismissActivity();return;}if(/INPUT|TEXTAREA|SELECT/.test(event.target.tagName)||state.busy)return;if(event.key.toLowerCase()==='d')setTool('draw');if(event.key==='Escape')setTool('select');if(event.key==='Delete'||event.key==='Backspace'){if(selected()){event.preventDefault();deleteRegion();}}}); let gradioClient = null; async function submitGradio(sid, body) { if(!gradioClient){ const {Client}=await import('/static/gradio-client.js'); gradioClient=await Client.connect(window.location.origin,{events:['data','status']}); } let result=null; const submission=gradioClient.submit('/refine_image',{sid,payload:JSON.stringify(body)}); for await(const event of submission){ if(event.type==='data')result=event.data[0]; if(event.type==='status'){ if(event.stage==='error')throw new Error(event.message||'The GPU request could not finish. Please retry.'); const update=event.progress_data?.at(-1); const queued=['pending','queued','joining_queue'].includes(event.stage); updateActivity(update?.desc||(queued?'Waiting for GPU':'Refining'),update?.progress,queued||update?.progress==null); } } if(!result){ let latest=await api(`/api/sessions/${sid}/job`); for(let attempt=0;attempt<150&&['queued','running'].includes(latest.status);attempt++){ await new Promise(resolve=>setTimeout(resolve,1000)); latest=await api(`/api/sessions/${sid}/jobs/${latest.id}`); } if(latest.status==='done')result={job_id:latest.id,result:latest.result}; else throw new Error(latest.message||'The request could not finish. Please retry.'); } return result; } async function runJob(kind) { if(state.busy||!state.session)return; const sid=state.session.id;notice();setBusy(true,kind==='detect'?'Preparing detection':'Waiting for GPU',kind); const body=kind==='detect'?{text_threshold:Number($('textThreshold').value),link_threshold:.2}:{regions:clone(state.regions),variant:state.model,steps:Number($('steps').value),seed:Number($('seed').value),guidance:Number($('guidance').value)}; try{ let job,job_id; if(kind==='refine'&&state.transport==='gradio'){ const completed=await submitGradio(sid,body);job_id=completed.job_id; job={status:'done',result:completed.result}; }else{ ({job_id}=await api(endpoint(kind),body)); do{ await new Promise(resolve=>setTimeout(resolve,650)); job=await api(`/api/sessions/${sid}/jobs/${job_id}`); updateActivity(job.message,job.progress,job.status==='queued'||kind==='detect'||job.progress<=0.04); }while(job.status==='queued'||job.status==='running'); } if(job.status==='error')throw new Error(job.message); if(kind==='detect'){ pushHistory();const previous=new Map(state.regions.map(r=>[r.id,r])); state.regions=job.result.regions.map(r=>previous.get(r.id)||r).concat(state.regions.filter(r=>r.origin==='manual')); state.selected=state.regions[0]?.id||null;render();changed();switchView('edit'); finishActivity('done',`${job.result.regions.length} regions · ${job.result.elapsed.toFixed(1)}s`); }else{state.result=job.result;state.stale=false;renderResults();switchView('compare');finishActivity('done',`${job.result.regions.length} regions · ${job.result.elapsed.toFixed(1)}s`);history.replaceState(null,'',`#s=${sid}&j=${job_id}${state.session.example?'&e='+state.session.example:''}`);} }catch(e){finishActivity('error',e.message);}finally{setBusy(false);renderList();} } $('detectButton').onclick=()=>runJob('detect');$('refineButton').onclick=()=>runJob('refine'); $('exampleTargets').onclick=async()=>{if(state.busy||!state.session)return;try{setBusy(true);const data=await api(endpoint('targets'));pushHistory();state.regions=data.regions;state.selected=data.regions[0]?.id||null;applySettings(data.settings);render();changed();switchView('edit');notice();}catch(e){notice(e.message);}finally{setBusy(false);}}; function renderResults(){ const result=state.result,base=endpoint('');$('compareView').disabled=false;$('comparePanel').classList.remove('result-stale'); $('beforeImage').src=state.session.image;$('afterImage').src=base+(result.preview||result.image); $('downloadImage').href=base+result.image;$('downloadRegions').href=base+result.metadata; $('resultSummary').textContent=`${result.elapsed.toFixed(1)}s`; $('resultSummary').title=`${result.model==='meanflow'?'MeanFlow':'Teacher'} · ${result.steps} steps`; $('regionResults').hidden=false;$('resultsCount').textContent='Before → After'; $('resultCards').innerHTML=result.regions.map((r,i)=>`
${String(i+1).padStart(2,'0')}${escape(r.text)}
BEFORE
Before: ${escape(r.text)}
AFTER
After: ${escape(r.text)}
`).join(''); } $('resultCards').addEventListener('click',event=>{const button=event.target.closest('[data-crop]');if(!button)return;const r=state.result.regions[Number(button.dataset.crop)];$('dialogTitle').textContent=r.text;$('dialogBefore').src=endpoint(r.before);$('dialogAfter').src=endpoint(r.after);$('cropDialog').showModal();}); $('closeDialog').onclick=()=>$('cropDialog').close(); $('compareSlider').oninput=()=>$('wipe').style.setProperty('--split',$('compareSlider').value+'%'); $('editView').onclick=()=>switchView('edit');$('compareView').onclick=()=>switchView('compare'); document.querySelectorAll('[data-model]').forEach(button=>button.onclick=()=>{if(state.busy)return;state.model=button.dataset.model;document.querySelectorAll('[data-model]').forEach(b=>{b.classList.toggle('active',b===button);b.setAttribute('aria-checked',String(b===button));});$('steps').value=state.model==='meanflow'?4:25;changed();}); ['steps','seed','guidance'].forEach(id=>$(id).onchange=changed); document.querySelectorAll('[data-example]').forEach(button=>button.onclick=()=>loadExample(button.dataset.example)); $('uploadButton').onclick=()=>$('imageUpload').click();$('imageUpload').onchange=()=>upload($('imageUpload').files[0]); $('dropZone').addEventListener('dragover',event=>{event.preventDefault();$('dropZone').classList.add('drop-hover');}); $('dropZone').addEventListener('dragleave',()=>$('dropZone').classList.remove('drop-hover')); $('dropZone').addEventListener('drop',event=>{event.preventDefault();$('dropZone').classList.remove('drop-hover');upload(event.dataTransfer.files[0]);}); async function openStudio(){ try{state.transport=(await api('/api/config')).transport||'http';}catch(e){notice('The studio could not connect. Please refresh the page.');return;} const params=new URLSearchParams(location.hash.slice(1)),sid=params.get('s'),jid=params.get('j'); if(sid&&jid&&/^[a-f0-9]{32}$/.test(sid)&&/^[a-f0-9]{32}$/.test(jid)){ try{ setBusy(true); const job=await api(`/api/sessions/${sid}/jobs/${jid}`); if(job.status!=='done'||job.kind!=='refine')throw new Error('This result is not available.'); const image=new Image();image.src=`/api/sessions/${sid}/preview.webp`;await image.decode(); await loadSession({id:sid,image:image.src,width:image.naturalWidth,height:image.naturalHeight,example:params.get('e')}); state.regions=job.result.regions.map(r=>({...r,enabled:true,origin:'saved'}));state.selected=state.regions[0]?.id||null; applySettings(job.result); state.result=job.result;render();renderResults();switchView('compare');return; }catch(e){notice('This preview has expired. Choose an example to start a new image.');} finally{setBusy(false);} } // Prepare both examples together; switching reuses decoded images and untouched presets. prepareExample('chinese').catch(()=>{}); await loadExample('english'); } openStudio();