Spaces:
Running on Zero
Running on Zero
Download static/app.js from lsmpp/PixText-Studio: direct link, hf CLI and curl.
- Browser
- Download file 25.2 kB
-
https://huggingface.co/spaces/lsmpp/PixText-Studio/resolve/main/static/app.js
- Command line
-
hf download hf://spaces/lsmpp/PixText-Studio/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/lsmpp/PixText-Studio/resolve/main/static/app.js
25.2 kB
| /* The editor stores original-image coordinates; canvas scaling never changes boxes. */ | |
| ; | |
| 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])=>`<rect data-id="${escape(r.id)}" data-handle="${c}" class="resize-handle ${c}" x="${a-size/2}" y="${b-size/2}" width="${size}" height="${size}"/>`).join(''):''; | |
| const label=(r.enabled||current)?`<rect data-id="${escape(r.id)}" class="region-label${status}" x="${x}" y="${labelY}" width="${badgeW}" height="${badgeH}" rx="${3*scale}"/><text class="region-label-text${status}" style="font-size:${11*scale}px" x="${x+badgeW/2}" y="${labelY+badgeH*.72}" text-anchor="middle">${i+1}</text>`:''; | |
| return `<g><rect data-id="${escape(r.id)}" class="region-rect${status}" x="${x}" y="${y}" width="${x1-x}" height="${y1-y}"/>${label}${handles}</g>`; | |
| }).join(''); | |
| } | |
| function renderList() { | |
| $('regionsEmpty').hidden=!!state.regions.length; | |
| $('regionList').innerHTML=state.regions.map((r,i)=>`<div class="region-row${r.id===state.selected?' selected':''}${r.enabled?' enabled':''}" data-id="${escape(r.id)}" role="button" tabindex="0" aria-label="Region ${i+1}: ${escape(r.text||'Enter target text')}"><input aria-label="Refine region ${i+1}" type="checkbox" ${r.enabled?'checked':''} ${state.busy?'disabled':''}><span class="region-num">${String(i+1).padStart(2,'0')}</span><span class="region-word">${escape(r.text||'Enter target text')}</span><span class="region-confidence">${r.origin==='example'?'example':r.origin==='manual'?'drawn':r.origin==='saved'?'saved':Math.round((r.confidence||0)*100)+'%'}</span></div>`).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)=>`<label>${label}<input data-coordinate="${index}" type="number" step="1" min="${index<2?0:1}" value="${Math.round(value)}" aria-label="Region ${label}" ${state.busy?'disabled':''}></label>`).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)=>`<article class="result-card"><header><span>${String(i+1).padStart(2,'0')}</span><strong>${escape(r.text)}</strong><button data-crop="${i}" title="Enlarge ${escape(r.text)}">⤢</button></header><div class="crop-pair"><figure><figcaption>BEFORE</figcaption><img src="${base+r.before}" alt="Before: ${escape(r.text)}"></figure><figure><figcaption>AFTER</figcaption><img src="${base+r.after}" alt="After: ${escape(r.text)}"></figure></div></article>`).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(); | |