Spaces:
Running
Running
Download app.js from wr92-course/test: direct link, hf CLI and curl.
- Browser
- Download file 16.9 kB
-
https://huggingface.co/spaces/wr92-course/test/resolve/main/app.js
- Command line
-
hf download hf://spaces/wr92-course/test/app.js
-
curl -L -o app.js https://huggingface.co/spaces/wr92-course/test/resolve/main/app.js
16.9 kB
| import { MODEL_OPTIONS, detectObjects, getModelOption } from './detector.js'; | |
| const imageInput = document.querySelector('#image-input'); | |
| const modelSelect = document.querySelector('#model-select'); | |
| const modelNote = document.querySelector('#model-note'); | |
| const threshold = document.querySelector('#threshold'); | |
| const thresholdValue = document.querySelector('#threshold-value'); | |
| const runButton = document.querySelector('#run-button'); | |
| const statusEl = document.querySelector('#status'); | |
| const progressWrap = document.querySelector('#progress-wrap'); | |
| const progressBar = document.querySelector('#progress-bar'); | |
| const canvasWrap = document.querySelector('#canvas-wrap'); | |
| const canvas = document.querySelector('#result-canvas'); | |
| const ctx = canvas.getContext('2d'); | |
| const summary = document.querySelector('#summary'); | |
| const tbody = document.querySelector('#detections-body'); | |
| const runtimeBadge = document.querySelector('#runtime-badge'); | |
| const sampleGrid = document.querySelector('#sample-grid'); | |
| const shuffleButton = document.querySelector('#shuffle-examples'); | |
| const state = { | |
| image: null, | |
| dataUrl: null, | |
| }; | |
| const EXAMPLES = [ | |
| { src: './examples/000000010092.jpg', label: 'Bedroom furniture' }, | |
| { src: './examples/000000037777.jpg', label: 'Kitchen objects' }, | |
| { src: './examples/000000039769.jpg', label: 'Cats & remotes' }, | |
| { src: './examples/000000063965.jpg', label: 'Cake & person' }, | |
| { src: './examples/000000087038.jpg', label: 'Skate park' }, | |
| { src: './examples/000000090208.jpg', label: 'Double-decker buses' }, | |
| { src: './examples/000000097278.jpg', label: 'Snowboarder' }, | |
| { src: './examples/000000167898.jpg', label: 'Bathroom sink & toilet' }, | |
| { src: './examples/000000174482.jpg', label: 'Bike & traffic' }, | |
| { src: './examples/000000181666.jpg', label: 'Herd & person' }, | |
| { src: './examples/000000189806.jpg', label: 'Cat & dog at home' }, | |
| { src: './examples/000000199442.jpg', label: 'Surfer' }, | |
| { src: './examples/000000210520.jpg', label: 'Dining table' }, | |
| { src: './examples/000000252219.jpg', label: 'Street scene' }, | |
| { src: './examples/000000286458.jpg', label: 'Packed suitcase' }, | |
| { src: './examples/000000365886.jpg', label: 'Person & cell phone' }, | |
| { src: './examples/000000376310.jpg', label: 'Bathroom objects' }, | |
| { src: './examples/000000397133.jpg', label: 'Busy kitchen' }, | |
| { src: './examples/000000429281.jpg', label: 'Bananas at market' }, | |
| { src: './examples/000000431876.jpg', label: 'Horses & riders' }, | |
| { src: './examples/000000482978.jpg', label: 'Passenger train' }, | |
| { src: './examples/000000500270.jpg', label: 'Skateboarder' }, | |
| { src: './examples/000000517523.jpg', label: 'Airplane landing' }, | |
| { src: './examples/000000570448.jpg', label: 'Boat at the beach' }, | |
| { src: './examples/000000382122.jpg', label: 'COCO · 382122' }, | |
| { src: './examples/000000021879.jpg', label: 'COCO · 021879' }, | |
| { src: './examples/000000124975.jpg', label: 'COCO · 124975' }, | |
| { src: './examples/000000507975.jpg', label: 'COCO · 507975' }, | |
| { src: './examples/000000364297.jpg', label: 'COCO · 364297' }, | |
| { src: './examples/000000273711.jpg', label: 'COCO · 273711' }, | |
| { src: './examples/000000230819.jpg', label: 'COCO · 230819' }, | |
| { src: './examples/000000033854.jpg', label: 'COCO · 033854' }, | |
| { src: './examples/000000064462.jpg', label: 'COCO · 064462' }, | |
| { src: './examples/000000175443.jpg', label: 'COCO · 175443' }, | |
| { src: './examples/000000132408.jpg', label: 'COCO · 132408' }, | |
| { src: './examples/000000456394.jpg', label: 'COCO · 456394' }, | |
| { src: './examples/000000422670.jpg', label: 'COCO · 422670' }, | |
| { src: './examples/000000550797.jpg', label: 'COCO · 550797' }, | |
| { src: './examples/000000061333.jpg', label: 'COCO · 061333' }, | |
| { src: './examples/000000071877.jpg', label: 'COCO · 071877' }, | |
| { src: './examples/000000038210.jpg', label: 'COCO · 038210' }, | |
| { src: './examples/000000220764.jpg', label: 'COCO · 220764' }, | |
| { src: './examples/000000561679.jpg', label: 'COCO · 561679' }, | |
| { src: './examples/000000229216.jpg', label: 'COCO · 229216' }, | |
| { src: './examples/000000462614.jpg', label: 'COCO · 462614' }, | |
| { src: './examples/000000204871.jpg', label: 'COCO · 204871' }, | |
| { src: './examples/000000546823.jpg', label: 'COCO · 546823' }, | |
| { src: './examples/000000454067.jpg', label: 'COCO · 454067' }, | |
| { src: './examples/000000361571.jpg', label: 'COCO · 361571' }, | |
| { src: './examples/000000140556.jpg', label: 'COCO · 140556' }, | |
| { src: './examples/000000175438.jpg', label: 'COCO · 175438' }, | |
| { src: './examples/000000297578.jpg', label: 'COCO · 297578' }, | |
| { src: './examples/000000162732.jpg', label: 'COCO · 162732' }, | |
| { src: './examples/000000147415.jpg', label: 'COCO · 147415' }, | |
| { src: './examples/000000458702.jpg', label: 'COCO · 458702' }, | |
| { src: './examples/000000416745.jpg', label: 'COCO · 416745' }, | |
| { src: './examples/000000122166.jpg', label: 'COCO · 122166' }, | |
| { src: './examples/000000438862.jpg', label: 'COCO · 438862' }, | |
| { src: './examples/000000445658.jpg', label: 'COCO · 445658' }, | |
| { src: './examples/000000465129.jpg', label: 'COCO · 465129' }, | |
| { src: './examples/000000546626.jpg', label: 'COCO · 546626' }, | |
| { src: './examples/000000442009.jpg', label: 'COCO · 442009' }, | |
| { src: './examples/000000166277.jpg', label: 'COCO · 166277' }, | |
| { src: './examples/000000017379.jpg', label: 'COCO · 017379' }, | |
| { src: './examples/000000053994.jpg', label: 'COCO · 053994' }, | |
| { src: './examples/000000372203.jpg', label: 'COCO · 372203' }, | |
| { src: './examples/000000028809.jpg', label: 'COCO · 028809' }, | |
| { src: './examples/000000163290.jpg', label: 'COCO · 163290' }, | |
| { src: './examples/000000133819.jpg', label: 'COCO · 133819' }, | |
| { src: './examples/000000239318.jpg', label: 'COCO · 239318' }, | |
| { src: './examples/000000116208.jpg', label: 'COCO · 116208' }, | |
| { src: './examples/000000106235.jpg', label: 'COCO · 106235' }, | |
| { src: './examples/000000464522.jpg', label: 'COCO · 464522' }, | |
| { src: './examples/000000147205.jpg', label: 'COCO · 147205' }, | |
| { src: './examples/000000575500.jpg', label: 'COCO · 575500' }, | |
| { src: './examples/000000005600.jpg', label: 'COCO · 005600' }, | |
| { src: './examples/000000462629.jpg', label: 'COCO · 462629' }, | |
| { src: './examples/000000499109.jpg', label: 'COCO · 499109' }, | |
| { src: './examples/000000066523.jpg', label: 'COCO · 066523' }, | |
| { src: './examples/000000228144.jpg', label: 'COCO · 228144' }, | |
| { src: './examples/000000161397.jpg', label: 'COCO · 161397' }, | |
| { src: './examples/000000082688.jpg', label: 'COCO · 082688' }, | |
| { src: './examples/000000305343.jpg', label: 'COCO · 305343' }, | |
| { src: './examples/000000150649.jpg', label: 'COCO · 150649' }, | |
| { src: './examples/000000253002.jpg', label: 'COCO · 253002' }, | |
| { src: './examples/000000410735.jpg', label: 'COCO · 410735' }, | |
| { src: './examples/000000363666.jpg', label: 'COCO · 363666' }, | |
| { src: './examples/000000514540.jpg', label: 'COCO · 514540' }, | |
| { src: './examples/000000546964.jpg', label: 'COCO · 546964' }, | |
| { src: './examples/000000301718.jpg', label: 'COCO · 301718' }, | |
| { src: './examples/000000402473.jpg', label: 'COCO · 402473' }, | |
| { src: './examples/000000566524.jpg', label: 'COCO · 566524' }, | |
| { src: './examples/000000468332.jpg', label: 'COCO · 468332' }, | |
| { src: './examples/000000215259.jpg', label: 'COCO · 215259' }, | |
| { src: './examples/000000067315.jpg', label: 'COCO · 067315' }, | |
| { src: './examples/000000055002.jpg', label: 'COCO · 055002' }, | |
| { src: './examples/000000555412.jpg', label: 'COCO · 555412' }, | |
| { src: './examples/000000227187.jpg', label: 'COCO · 227187' }, | |
| { src: './examples/000000047571.jpg', label: 'COCO · 047571' }, | |
| { src: './examples/000000231580.jpg', label: 'COCO · 231580' }, | |
| ]; | |
| function chooseRandom(items, count) { | |
| return [...items] | |
| .map((item) => ({ item, sort: Math.random() })) | |
| .sort((a, b) => a.sort - b.sort) | |
| .slice(0, count) | |
| .map(({ item }) => item); | |
| } | |
| function resetResults() { | |
| summary.textContent = 'No detections yet.'; | |
| tbody.innerHTML = '<tr class="empty-row"><td colspan="3">Run the detector to see details.</td></tr>'; | |
| } | |
| function setSelectedImage(dataUrl, readyMessage) { | |
| const image = new Image(); | |
| image.onload = () => { | |
| state.dataUrl = dataUrl; | |
| state.image = image; | |
| drawBaseImage(); | |
| runButton.disabled = false; | |
| statusEl.textContent = readyMessage; | |
| setBadge('Image ready'); | |
| resetResults(); | |
| }; | |
| image.onerror = () => { | |
| statusEl.textContent = 'Could not read that image.'; | |
| setBadge('Image error', 'error'); | |
| }; | |
| image.src = dataUrl; | |
| } | |
| function blobToDataUrl(blob) { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = () => resolve(reader.result); | |
| reader.onerror = reject; | |
| reader.readAsDataURL(blob); | |
| }); | |
| } | |
| async function loadExample(example, button) { | |
| try { | |
| statusEl.textContent = `Loading COCO example: ${example.label}…`; | |
| setBadge('Loading sample', 'running'); | |
| const response = await fetch(example.src); | |
| if (!response.ok) throw new Error(`Could not load ${example.src}`); | |
| const dataUrl = await blobToDataUrl(await response.blob()); | |
| imageInput.value = ''; | |
| document.querySelectorAll('.sample-card').forEach((card) => card.classList.remove('selected')); | |
| button.classList.add('selected'); | |
| setSelectedImage(dataUrl, `COCO example ready: ${example.label}. Click Run AI Detection.`); | |
| } catch (error) { | |
| console.error(error); | |
| statusEl.textContent = 'Could not load that COCO example. Try another one.'; | |
| setBadge('Sample error', 'error'); | |
| } | |
| } | |
| function renderModelChoices() { | |
| for (const option of MODEL_OPTIONS) { | |
| const item = document.createElement('option'); | |
| item.value = option.key; | |
| item.textContent = option.label; | |
| modelSelect.appendChild(item); | |
| } | |
| modelSelect.value = MODEL_OPTIONS[0].key; | |
| modelNote.textContent = MODEL_OPTIONS[0].note; | |
| } | |
| function renderExampleChoices() { | |
| sampleGrid.innerHTML = ''; | |
| for (const example of chooseRandom(EXAMPLES, 4)) { | |
| const button = document.createElement('button'); | |
| button.type = 'button'; | |
| button.className = 'sample-card'; | |
| button.title = `Use sample: ${example.label}`; | |
| const image = document.createElement('img'); | |
| image.src = example.src; | |
| image.alt = example.label; | |
| image.loading = 'lazy'; | |
| const label = document.createElement('span'); | |
| label.textContent = example.label; | |
| const source = document.createElement('small'); | |
| const cocoId = example.src.match(/(\d{12})\.jpg$/)?.[1] || ''; | |
| source.textContent = cocoId ? `COCO val2017 · ${cocoId}` : 'COCO val2017'; | |
| button.append(image, label, source); | |
| button.addEventListener('click', () => loadExample(example, button)); | |
| sampleGrid.appendChild(button); | |
| } | |
| } | |
| function setBadge(text, mode = '') { | |
| runtimeBadge.textContent = text; | |
| runtimeBadge.className = `badge ${mode}`.trim(); | |
| } | |
| function setProgress(data) { | |
| let pct = null; | |
| if (typeof data?.progress === 'number') { | |
| pct = data.progress; | |
| } else if (data?.total && typeof data?.loaded === 'number') { | |
| pct = (data.loaded / data.total) * 100; | |
| } | |
| if (pct !== null && Number.isFinite(pct)) { | |
| progressWrap.hidden = false; | |
| progressBar.style.width = `${Math.max(0, Math.min(100, pct))}%`; | |
| } | |
| if (data?.status === 'progress' && data?.file) { | |
| statusEl.textContent = `Downloading model: ${data.file}`; | |
| } | |
| } | |
| function drawBaseImage() { | |
| if (!state.image) return; | |
| canvas.width = state.image.naturalWidth; | |
| canvas.height = state.image.naturalHeight; | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| ctx.drawImage(state.image, 0, 0, canvas.width, canvas.height); | |
| canvasWrap.classList.remove('empty'); | |
| } | |
| function drawDetections(detections) { | |
| drawBaseImage(); | |
| if (!state.image) return; | |
| const scale = Math.max(1, Math.round(canvas.width / 900)); | |
| const fontSize = Math.max(14, Math.round(canvas.width / 55)); | |
| ctx.font = `700 ${fontSize}px system-ui, sans-serif`; | |
| ctx.textBaseline = 'top'; | |
| for (const item of detections) { | |
| const box = item.box; | |
| const x1 = Math.max(0, Number(box.xmin)); | |
| const y1 = Math.max(0, Number(box.ymin)); | |
| const x2 = Math.min(canvas.width, Number(box.xmax)); | |
| const y2 = Math.min(canvas.height, Number(box.ymax)); | |
| const label = `${item.label} ${(Number(item.score) * 100).toFixed(0)}%`; | |
| ctx.lineWidth = 3 * scale; | |
| ctx.strokeStyle = '#ffffff'; | |
| ctx.strokeRect(x1, y1, Math.max(0, x2 - x1), Math.max(0, y2 - y1)); | |
| const pad = 5 * scale; | |
| const metrics = ctx.measureText(label); | |
| const textHeight = fontSize + 2 * pad; | |
| const labelY = Math.max(0, y1 - textHeight); | |
| ctx.fillStyle = '#111827'; | |
| ctx.fillRect(x1, labelY, metrics.width + 2 * pad, textHeight); | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillText(label, x1 + pad, labelY + pad); | |
| } | |
| } | |
| function renderDetections(detections) { | |
| tbody.innerHTML = ''; | |
| const option = getModelOption(modelSelect.value); | |
| if (!detections.length) { | |
| const row = document.createElement('tr'); | |
| row.className = 'empty-row'; | |
| const cell = document.createElement('td'); | |
| cell.colSpan = 3; | |
| cell.textContent = 'No objects passed the current confidence threshold.'; | |
| row.appendChild(cell); | |
| tbody.appendChild(row); | |
| summary.textContent = `${option.label}: no detections. Try a lower threshold or another image.`; | |
| return; | |
| } | |
| const counts = new Map(); | |
| for (const item of detections) { | |
| counts.set(item.label, (counts.get(item.label) || 0) + 1); | |
| const row = document.createElement('tr'); | |
| const box = item.box; | |
| const values = [ | |
| item.label, | |
| `${(Number(item.score) * 100).toFixed(1)}%`, | |
| `(${Math.round(box.xmin)}, ${Math.round(box.ymin)}) → (${Math.round(box.xmax)}, ${Math.round(box.ymax)})`, | |
| ]; | |
| for (const value of values) { | |
| const cell = document.createElement('td'); | |
| cell.textContent = value; | |
| row.appendChild(cell); | |
| } | |
| tbody.appendChild(row); | |
| } | |
| const pieces = [...counts.entries()] | |
| .sort(([a], [b]) => a.localeCompare(b)) | |
| .map(([label, count]) => `${label}: ${count}`); | |
| summary.textContent = `${option.label} found — ${pieces.join(', ')}`; | |
| } | |
| threshold.addEventListener('input', () => { | |
| thresholdValue.value = Number(threshold.value).toFixed(2); | |
| }); | |
| modelSelect.addEventListener('change', () => { | |
| const option = getModelOption(modelSelect.value); | |
| modelNote.textContent = option.note; | |
| setBadge(`Model: ${option.label}`); | |
| if (state.image) statusEl.textContent = `Model changed to ${option.label}. Run again to compare results.`; | |
| }); | |
| imageInput.addEventListener('change', () => { | |
| const file = imageInput.files?.[0]; | |
| if (!file) return; | |
| if (!file.type.startsWith('image/')) { | |
| statusEl.textContent = 'Please choose an image file.'; | |
| setBadge('Invalid file', 'error'); | |
| runButton.disabled = true; | |
| return; | |
| } | |
| document.querySelectorAll('.sample-card').forEach((card) => card.classList.remove('selected')); | |
| const reader = new FileReader(); | |
| reader.onload = () => setSelectedImage(reader.result, 'Your image is ready. Click Run AI Detection.'); | |
| reader.onerror = () => { | |
| statusEl.textContent = 'Could not read that image.'; | |
| setBadge('Image error', 'error'); | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| shuffleButton.addEventListener('click', () => { | |
| renderExampleChoices(); | |
| statusEl.textContent = state.image | |
| ? 'New COCO examples shown. Your current image is still selected.' | |
| : 'Choose a COCO example, or upload your own image.'; | |
| }); | |
| renderModelChoices(); | |
| renderExampleChoices(); | |
| runButton.addEventListener('click', async () => { | |
| if (!state.dataUrl || !state.image) return; | |
| const option = getModelOption(modelSelect.value); | |
| runButton.disabled = true; | |
| setBadge(`Loading ${option.label}`, 'running'); | |
| progressWrap.hidden = false; | |
| progressBar.style.width = '4%'; | |
| const started = performance.now(); | |
| try { | |
| statusEl.textContent = `Preparing ${option.label}. The first run may download model files.`; | |
| const detections = await detectObjects(state.dataUrl, { | |
| modelKey: option.key, | |
| threshold: Number(threshold.value), | |
| progressCallback: setProgress, | |
| }); | |
| drawDetections(detections); | |
| renderDetections(detections); | |
| const seconds = ((performance.now() - started) / 1000).toFixed(1); | |
| statusEl.textContent = `Done in ${seconds}s with ${option.label}. Change model or threshold to compare.`; | |
| setBadge(`${detections.length} detections`, 'ready'); | |
| } catch (error) { | |
| console.error(error); | |
| statusEl.textContent = `${option.label} failed to run. Check your connection or switch back to RT-DETR.`; | |
| setBadge('Error', 'error'); | |
| } finally { | |
| progressWrap.hidden = true; | |
| runButton.disabled = false; | |
| } | |
| }); | |