Spaces:
Running on Zero
Running on Zero
Download assets/console.js from FanLR/GroundingPI: direct link, hf CLI and curl.
- Browser
- Download file 17.3 kB
-
https://huggingface.co/spaces/FanLR/GroundingPI/resolve/main/assets/console.js
- Command line
-
hf download hf://spaces/FanLR/GroundingPI/assets/console.js
-
curl -L -o console.js https://huggingface.co/spaces/FanLR/GroundingPI/resolve/main/assets/console.js
17.3 kB
| () => { | |
| // Progressive enhancement: inference and view controls work without this script. | |
| window.groundingConsoleCleanup?.(); | |
| const reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); | |
| const abort = new AbortController(); | |
| const listen = (el, event, fn) => el.addEventListener(event, fn, { signal: abort.signal }); | |
| let observer, resize, frame, interval, selected, targetRoot, image, lastSrc, lastDraw; | |
| let waiting = false, waitStart = 0, ready = false, watchdog, instanceIndex = 0; | |
| let clientPending = false, previousRunId = '', clientNotice; | |
| let zoom = 1, panX = 0, panY = 0, drag, moved = false; | |
| const $ = s => document.querySelector(s); | |
| const svg = (tag, attrs = {}) => { | |
| const el = document.createElementNS('http://www.w3.org/2000/svg', tag); | |
| Object.entries(attrs).forEach(([name, value]) => el.setAttribute(name, String(value))); | |
| return el; | |
| }; | |
| function removeLayer() { | |
| $('#stage-image .focus-layer')?.remove(); | |
| $('#stage-image .focus-pixels')?.remove(); | |
| $('#canvas-viewport .focus-caption')?.remove(); | |
| lastDraw = null; | |
| } | |
| function clearSelection() { | |
| selected = null; | |
| instanceIndex = 0; | |
| document.querySelectorAll('.target-chip[aria-pressed=true]').forEach(b => b.setAttribute('aria-pressed', 'false')); | |
| const clear = $('.clear-focus'); | |
| if (clear) clear.hidden = true; | |
| removeLayer(); | |
| const detail = $('.evidence-detail'); if (detail) detail.hidden = true; | |
| } | |
| function showDetail(coords) { | |
| const detail = $('.evidence-detail'), preview = $('.detail-image'); | |
| if (!detail || !preview || !image?.naturalWidth) return; | |
| detail.hidden = false; | |
| const p = coords[instanceIndex]; | |
| const box = p.length === 4 ? p : [Math.max(0,p[0]-65), Math.max(0,p[1]-65), Math.min(999,p[0]+65), Math.min(999,p[1]+65)]; | |
| const iw = image.naturalWidth, ih = image.naturalHeight; | |
| const cx = (box[0]+box[2])/1998*iw, cy = (box[1]+box[3])/1998*ih; | |
| const cw = Math.max(32,(box[2]-box[0])/999*iw*1.3), ch = Math.max(32,(box[3]-box[1])/999*ih*1.3); | |
| const scale = Math.min(preview.clientWidth/cw, preview.clientHeight/ch, 8); | |
| const source = $('.focus-source'); | |
| preview.style.backgroundImage = `url(${JSON.stringify(source?.src || image.currentSrc || image.src)})`; | |
| preview.style.backgroundSize = `${iw*scale}px ${ih*scale}px`; | |
| preview.style.backgroundPosition = `${preview.clientWidth/2-cx*scale}px ${preview.clientHeight/2-cy*scale}px`; | |
| $('.detail-coordinates').textContent = `Instance ${instanceIndex + 1} / ${coords.length} · [${p.join(', ')}]`; | |
| } | |
| function canvasTransform() { | |
| const stage = $('#stage-image'); if (!stage) return; | |
| const maxX = stage.clientWidth*(zoom-1)/2, maxY = stage.clientHeight*(zoom-1)/2; | |
| panX = Math.max(-maxX,Math.min(maxX,panX)); panY = Math.max(-maxY,Math.min(maxY,panY)); | |
| stage.style.transform = `translate(${panX}px,${panY}px) scale(${zoom})`; | |
| $('#canvas-viewport')?.classList.toggle('is-zoomed',zoom>1); | |
| const value = $('.zoom-value'); if (value) value.textContent = `${Math.round(zoom*100)}%`; | |
| const out = $('[data-canvas=out]'), up = $('[data-canvas=in]'); | |
| if (out) out.disabled = zoom<=1; if (up) up.disabled = zoom>=4; | |
| } | |
| function resetCanvas() { zoom=1; panX=panY=0; canvasTransform(); } | |
| function drawSelection() { | |
| const stage = $('#stage-image'); | |
| if (!selected?.isConnected || !image?.complete || !image.naturalWidth || waiting) return; | |
| let coords; | |
| try { coords = JSON.parse(selected.dataset.coordinates); } catch { clearSelection(); return; } | |
| if (!Array.isArray(coords) || !coords.length || !coords.every(p => [2, 4].includes(p.length) && p.every(n => Number.isFinite(n) && n >= 0 && n <= 999))) { | |
| clearSelection(); return; | |
| } | |
| const rect = image.getBoundingClientRect(), outer = stage.getBoundingClientRect(); | |
| const matrix = getComputedStyle(stage).transform; | |
| const zoomNow = matrix === 'none' ? 1 : new DOMMatrixReadOnly(matrix).a; | |
| const scale = Math.min(rect.width / zoomNow / image.naturalWidth, rect.height / zoomNow / image.naturalHeight); | |
| const w = image.naturalWidth * scale, h = image.naturalHeight * scale; | |
| if (w <= 0 || h <= 0) return; | |
| const left = (rect.left - outer.left) / zoomNow + (rect.width / zoomNow - w) / 2; | |
| const top = (rect.top - outer.top) / zoomNow + (rect.height / zoomNow - h) / 2; | |
| instanceIndex = Math.max(0, Math.min(instanceIndex, coords.length - 1)); | |
| const signature = JSON.stringify([selected.dataset.label, instanceIndex, coords, image.currentSrc, left, top, w, h]); | |
| if (signature === lastDraw) return; | |
| const activeAction = document.activeElement?.closest('.focus-caption button')?.dataset.instance; | |
| removeLayer(); lastDraw = signature; | |
| const source = $('.focus-source'); | |
| if (source) { | |
| const pixels = document.createElement('img'); | |
| pixels.className = 'focus-pixels'; pixels.alt = ''; pixels.setAttribute('aria-hidden', 'true'); pixels.src = source.src; | |
| Object.assign(pixels.style, {left: `${left}px`, top: `${top}px`, width: `${w}px`, height: `${h}px`}); | |
| stage.append(pixels); | |
| } | |
| const layer = svg('svg', { viewBox: `0 0 ${w} ${h}`, 'aria-hidden': 'true', class: 'focus-layer' }); | |
| Object.assign(layer.style, { left: `${left}px`, top: `${top}px`, width: `${w}px`, height: `${h}px` }); | |
| const defs = svg('defs'), mask = svg('mask', { id: 'grounding-focus-mask', maskUnits: 'userSpaceOnUse', x: 0, y: 0, width: w, height: h }); | |
| mask.append(svg('rect', { width: w, height: h, fill: 'white' })); | |
| const marks = []; | |
| for (const p of [coords[instanceIndex]]) { | |
| const attr = p.length === 4 | |
| ? { x: p[0] / 999 * w, y: p[1] / 999 * h, width: (p[2] - p[0]) / 999 * w, height: (p[3] - p[1]) / 999 * h, rx: 2 } | |
| : { cx: p[0] / 999 * w, cy: p[1] / 999 * h, r: 13 }; | |
| const tag = p.length === 4 ? 'rect' : 'circle'; | |
| mask.append(svg(tag, { ...attr, fill: 'black' })); | |
| const outline = svg(tag, { ...attr, class: 'focus-outline', pathLength: 1 }); | |
| outline.style.stroke = getComputedStyle(selected.querySelector('i')).backgroundColor; | |
| marks.push(svg(tag, { ...attr, class: 'focus-halo' }), outline); | |
| } | |
| defs.append(mask); layer.append(defs); | |
| layer.append(svg('rect', { width: w, height: h, mask: 'url(#grounding-focus-mask)', class: 'focus-dimmer' }), ...marks); | |
| const caption = document.createElement('div'); caption.className = 'focus-caption'; | |
| const label = document.createElement('span'); | |
| label.setAttribute('role', 'status'); label.setAttribute('aria-live', 'polite'); | |
| const evidence = 'Prediction'; | |
| label.textContent = `${evidence} · ${selected.dataset.label} · ${instanceIndex + 1} / ${coords.length}`; | |
| label.title = label.textContent; | |
| caption.append(label); | |
| for (const [action, name, path] of [['prev', 'Previous instance', 'm12 5-5 5 5 5'], ['next', 'Next instance', 'm8 5 5 5-5 5']]) { | |
| const button = document.createElement('button'); button.type = 'button'; button.dataset.instance = action; | |
| button.setAttribute('aria-label', name); button.title = name; button.disabled = coords.length < 2; | |
| const icon = svg('svg', {viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', 'aria-hidden': 'true'}); | |
| icon.append(svg('path', {d: path, stroke: 'currentColor', 'stroke-width': 1.7})); | |
| button.append(icon); caption.append(button); | |
| } | |
| const close = document.createElement('button'); close.type = 'button'; close.className = 'focus-close'; close.setAttribute('aria-label', 'Close target focus'); | |
| const icon = svg('svg', { viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', 'aria-hidden': 'true' }); | |
| icon.append(svg('path', { d: 'm6 6 8 8m0-8-8 8', stroke: 'currentColor', 'stroke-width': 1.7, 'stroke-linecap': 'round' })); | |
| close.title = 'Close focus and return to targets'; | |
| close.append(icon); caption.append(close); stage.append(layer); $('#canvas-viewport').append(caption); | |
| if (activeAction) caption.querySelector(`[data-instance=${activeAction}]`)?.focus({preventScroll: true}); | |
| showDetail(coords); | |
| } | |
| function tick() { | |
| if (document.hidden || !waiting) return; | |
| const clock = $('.wait-clock'); | |
| if (clock) clock.textContent = `${((performance.now() - waitStart) / 1000).toFixed(1)} s`; | |
| } | |
| function setWaiting(value) { | |
| if (value === waiting) return; | |
| waiting = value; | |
| $('#console').setAttribute('aria-busy', String(value)); | |
| for (const button of document.querySelectorAll('#run-button,#mobile-run-button')) { | |
| button.disabled = value; | |
| button.classList.toggle('is-waiting', value); | |
| if (value) button.setAttribute('aria-label', 'Grounding in progress'); | |
| else button.removeAttribute('aria-label'); | |
| } | |
| for (const id of ['controls', 'sandbox-panel', 'query-dock', 'upload-button', 'canvas-chrome']) { | |
| const el = $(`#${id}`); if (el) el.inert = value; | |
| } | |
| clearInterval(interval); | |
| if (!value) clearTimeout(watchdog); | |
| if (value) { clearSelection(); resetCanvas(); waitStart = performance.now(); interval = setInterval(tick, 100); } | |
| } | |
| function clientStatus(text, phase) { | |
| const node = document.createElement('div'); | |
| node.className = `stage-status ${phase}`; node.dataset.phase = phase; | |
| node.setAttribute('role', 'status'); node.setAttribute('aria-live', 'polite'); | |
| const dot = document.createElement('i'); dot.setAttribute('aria-hidden', 'true'); | |
| const label = document.createElement('span'); label.textContent = text; | |
| node.append(dot, label); | |
| if (phase === 'busy') { | |
| const clock = document.createElement('time'); clock.className = 'wait-clock'; | |
| clock.setAttribute('aria-hidden', 'true'); clock.textContent = '0.0 s'; node.append(clock); | |
| } | |
| // This node belongs to us, outside Gradio's HTML component. Never replace | |
| // Svelte-owned children: doing so can leave an orphan busy status forever. | |
| clientNotice?.remove(); clientNotice = node; | |
| node.classList.add('client-notice'); | |
| $('#stage-toolbar').append(node); | |
| $('#stage-toolbar').classList.add('client-pending'); | |
| } | |
| // A reverse proxy can buffer the first SSE event. Acknowledge the actual click | |
| // immediately; backend events still own final results and measured latency. | |
| window.groundingConsoleBegin = () => { | |
| if (!ready || waiting) return; | |
| previousRunId = $('#stage-status .stage-status')?.dataset.runId || ''; | |
| clientPending = true; | |
| clientStatus('Request submitted · Awaiting inference', 'busy'); setWaiting(true); | |
| watchdog = setTimeout(() => { | |
| if (!waiting) return; | |
| clientStatus('No result received. Check the connection and refresh. The server request has not been cancelled.', 'error'); | |
| setWaiting(false); | |
| }, 330000); | |
| }; | |
| function schedule() { | |
| if (frame) return; | |
| frame = requestAnimationFrame(() => { frame = null; reconcile(); }); | |
| } | |
| function reconcile() { | |
| const stage = $('#stage-image'), targets = $('#targets-content'); | |
| if (!stage || !targets) return; | |
| const server = $('#stage-status .stage-status'); | |
| if (clientPending && (server?.dataset.runId || '') !== previousRunId) { | |
| clientPending = false; | |
| clientNotice?.remove(); clientNotice = null; | |
| $('#stage-toolbar').classList.remove('client-pending'); | |
| } | |
| if (!clientPending) setWaiting(server?.dataset.phase === 'busy'); | |
| const chips = targets.querySelector('.target-chips'); | |
| if (targetRoot !== chips) { targetRoot = chips; clearSelection(); } | |
| const nextImage = stage.querySelector('img'); | |
| if (image !== nextImage) { image = nextImage; if (image) listen(image, 'load', schedule); } | |
| const src = image?.currentSrc || image?.src; | |
| const download = $('.canvas-download'); | |
| if (download && src) download.href = src; | |
| if (src !== lastSrc) { | |
| clearSelection(); resetCanvas(); lastSrc = src; | |
| } | |
| const dimensions = $('.image-dimensions'); | |
| if (dimensions && image?.naturalWidth) dimensions.textContent = `${image.naturalWidth} × ${image.naturalHeight} px${zoom>1 ? ' / drag to pan' : ''}`; | |
| drawSelection(); | |
| } | |
| function onClick(event) { | |
| // The native click has already reached Gradio's button handler. Keep its | |
| // State inputs untouched; only enhance presentation during network latency. | |
| if (event.target.closest?.('#run-button,#mobile-run-button')) window.groundingConsoleBegin?.(); | |
| const instance = event.target.closest?.('[data-instance]')?.dataset.instance; | |
| if (instance && selected && !waiting) { | |
| const count = JSON.parse(selected.dataset.coordinates).length; | |
| instanceIndex = (instanceIndex + (instance === 'next' ? 1 : -1) + count) % count; | |
| drawSelection(); return; | |
| } | |
| const action = event.target.closest?.('[data-canvas]')?.dataset.canvas; | |
| if (action && !waiting) { | |
| if (action === 'fit') resetCanvas(); | |
| else { zoom=Math.max(1,Math.min(4,zoom+(action==='in'?.5:-.5))); canvasTransform(); } | |
| schedule(); return; | |
| } | |
| const chip = event.target.closest?.('#targets-content .target-chip'); | |
| if (chip && !waiting) { | |
| const wasSelected = chip === selected; | |
| clearSelection(); | |
| if (!wasSelected) { | |
| selected = chip; chip.setAttribute('aria-pressed', 'true'); $('.clear-focus').hidden = false; drawSelection(); | |
| const bounds = $('#canvas-viewport').getBoundingClientRect(); | |
| if (bounds.top < 0 || bounds.bottom > innerHeight) { | |
| $('#canvas-viewport').scrollIntoView({block: 'center', behavior: reduced.matches ? 'instant' : 'smooth'}); | |
| $('.focus-close')?.focus({preventScroll: true}); | |
| } | |
| } | |
| } else if (event.target.closest?.('.clear-focus,.focus-close')) { | |
| const previous = selected; clearSelection(); previous?.focus(); | |
| } | |
| // Select an actual returned box directly on the image; no invented geometry. | |
| if (!waiting && !moved && event.target === image) { | |
| const rect=image.getBoundingClientRect(), scale=Math.min(rect.width/image.naturalWidth,rect.height/image.naturalHeight); | |
| const w=image.naturalWidth*scale, h=image.naturalHeight*scale; | |
| const x=(event.clientX-rect.left-(rect.width-w)/2)/w*999, y=(event.clientY-rect.top-(rect.height-h)/2)/h*999; | |
| const hits=[]; | |
| document.querySelectorAll('.target-chip').forEach(chip=>{ | |
| let coords; try{coords=JSON.parse(chip.dataset.coordinates);}catch{return;} | |
| coords.forEach((p,index)=>{ | |
| if (p.length===4 && x>=p[0] && x<=p[2] && y>=p[1] && y<=p[3]) hits.push({chip,index,size:(p[2]-p[0])*(p[3]-p[1])}); | |
| if (p.length===2 && Math.hypot(x-p[0],y-p[1])<20) hits.push({chip,index,size:0}); | |
| }); | |
| }); | |
| const hit=hits.sort((a,b)=>a.size-b.size)[0]; | |
| if(hit){clearSelection();selected=hit.chip;instanceIndex=hit.index;selected.setAttribute('aria-pressed','true');$('.clear-focus').hidden=false;drawSelection();$('.focus-close')?.focus({preventScroll:true});} | |
| } | |
| moved=false; | |
| } | |
| function init() { | |
| if (ready || !$('#console') || !$('#targets-content') || !$('#stage-status')) return; | |
| ready = true; observer.disconnect(); | |
| listen(document, 'click', onClick); | |
| listen(document, 'change', schedule); | |
| listen(document, 'keydown', e => { if (e.key === 'Escape') {const previous=selected;clearSelection();resetCanvas();previous?.focus();} }); | |
| const viewport=$('#canvas-viewport'); | |
| listen(viewport,'pointerdown',e=>{ | |
| moved=false; | |
| if(zoom<=1 || waiting || e.button!==0 || e.target.closest('button') || !e.target.closest('#stage-image')) return; | |
| drag={x:e.clientX,y:e.clientY,px:panX,py:panY}; viewport.setPointerCapture(e.pointerId); viewport.classList.add('is-panning'); e.preventDefault(); | |
| }); | |
| listen(viewport,'pointermove',e=>{ | |
| if(!drag)return; | |
| const dx=e.clientX-drag.x,dy=e.clientY-drag.y; moved ||= Math.hypot(dx,dy)>4; | |
| panX=drag.px+dx;panY=drag.py+dy;canvasTransform(); | |
| }); | |
| const endDrag=()=>{drag=null;viewport.classList.remove('is-panning');}; | |
| listen(viewport,'pointerup',endDrag);listen(viewport,'pointercancel',endDrag); | |
| listen(viewport,'dragstart',e=>{if(zoom>1)e.preventDefault();}); | |
| listen(document, 'visibilitychange', () => { $('#console')?.classList.toggle('tab-hidden', document.hidden); tick(); }); | |
| observer = new MutationObserver(records => { | |
| if (records.some(r => !r.target.closest?.('.focus-layer,.focus-caption,.wait-clock'))) schedule(); | |
| }); | |
| for (const id of ['stage-image', 'targets-content', 'stage-status']) observer.observe($(`#${id}`), { childList: true, subtree: true, attributes: true, attributeFilter: ['src'] }); | |
| resize = new ResizeObserver(schedule); resize.observe($('#stage-image')); resize.observe($('#controls')); reconcile(); | |
| document.fonts.ready.then(schedule); | |
| } | |
| observer = new MutationObserver(init); observer.observe(document.body, { childList: true, subtree: true }); init(); | |
| window.groundingConsoleCleanup = () => { | |
| abort.abort(); observer?.disconnect(); resize?.disconnect(); cancelAnimationFrame(frame); clearInterval(interval); clearTimeout(watchdog); | |
| if (ready) setWaiting(false); removeLayer(); | |
| clientNotice?.remove(); $('#stage-toolbar')?.classList.remove('client-pending'); | |
| delete window.groundingConsoleBegin; | |
| }; | |
| } | |