Download index.html from roop07/wer: direct link, hf CLI and curl.
- Browser
- Download file 15.3 kB
-
https://huggingface.co/spaces/roop07/wer/resolve/main/index.html
- Command line
-
hf download hf://spaces/roop07/wer/index.html
-
curl -L -o index.html https://huggingface.co/spaces/roop07/wer/resolve/main/index.html
15.3 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> | |
| <title>Comic panel detector</title> | |
| <style> | |
| :root { | |
| box-sizing: border-box; | |
| padding-top: env(safe-area-inset-top, 0px); | |
| padding-bottom: env(safe-area-inset-bottom, 0px); | |
| --surface-0: #FAFAF8; | |
| --surface-1: #F0EFEA; | |
| --surface-2: #E4E2DA; | |
| --text-primary: #1A1A18; | |
| --text-secondary: #5C5A52; | |
| --text-muted: #8A877C; | |
| --border-strong: #C9C6BA; | |
| --border-accent: #185FA5; | |
| --radius: 10px; | |
| --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; | |
| --bg2-light: #E6F1FB; --bg2-dark: #042C53; | |
| --accent-light: #185FA5; --accent-dark: #85B7EB; | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| :root { | |
| --surface-0: #161512; | |
| --surface-1: #201F1B; | |
| --surface-2: #2C2A24; | |
| --text-primary: #F0EFEA; | |
| --text-secondary: #A8A599; | |
| --text-muted: #726F63; | |
| --border-strong: #3A382F; | |
| --border-accent: #85B7EB; | |
| } | |
| } | |
| html { scroll-padding-top: env(safe-area-inset-top, 0px); } | |
| *, *::before, *::after { box-sizing: inherit; } | |
| body { background: var(--surface-0); color: var(--text-primary); font-family: var(--font-sans); margin: 0; padding: 1rem 0; } | |
| .wrap { max-width: 480px; margin: 0 auto; padding: 0 16px; } | |
| h2 { font-size: 18px; font-weight: 500; margin: 0 0 4px; } | |
| p.sub { font-size: 13px; color: var(--text-secondary); margin: 0 0 20px; line-height: 1.5; } | |
| .upload-btn { | |
| display: flex; align-items: center; justify-content: center; gap: 8px; | |
| width: 100%; padding: 14px; border-radius: var(--radius); | |
| border: 1px dashed var(--border-strong); background: var(--surface-1); | |
| color: var(--text-primary); font-size: 14px; cursor: pointer; | |
| } | |
| .upload-btn:active { background: var(--surface-2); } | |
| input[type=file] { display: none; } | |
| .thumbs { display: flex; gap: 8px; overflow-x: auto; margin: 14px 0; padding-bottom: 4px; } | |
| .thumb { flex: 0 0 auto; width: 56px; height: 56px; border-radius: 8px; overflow: hidden; | |
| border: 2px solid transparent; cursor: pointer; background: var(--surface-1); } | |
| .thumb.active { border-color: var(--border-accent); } | |
| .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; } | |
| .canvas-card { background: var(--surface-1); border-radius: 12px; overflow: hidden; margin: 12px 0; } | |
| .canvas-scroll { overflow: auto; max-height: 60vh; } | |
| canvas { display: block; width: 100%; height: auto; } | |
| .controls { background: var(--surface-1); border-radius: 12px; padding: 16px; margin: 12px 0; } | |
| .ctrl-row { margin-bottom: 14px; } | |
| .ctrl-row:last-child { margin-bottom: 0; } | |
| .ctrl-label { display: flex; justify-content: space-between; font-size: 13px; color: var(--text-secondary); margin-bottom: 6px; } | |
| .ctrl-label span:last-child { color: var(--text-primary); font-weight: 500; } | |
| input[type=range] { width: 100%; accent-color: var(--accent-light); } | |
| @media (prefers-color-scheme: dark) { input[type=range] { accent-color: var(--accent-dark); } } | |
| [data-mode="dark"] input[type=range] { accent-color: var(--accent-dark); } | |
| .toggle-row { display: flex; align-items: center; justify-content: space-between; } | |
| .toggle-row .ctrl-label { margin-bottom: 0; } | |
| .switch { position: relative; width: 40px; height: 24px; flex: 0 0 auto; } | |
| .switch input { display: none; } | |
| .switch .track { position: absolute; inset: 0; background: var(--border-strong); border-radius: 12px; transition: background .15s; } | |
| .switch input:checked + .track { background: var(--accent-light); } | |
| [data-mode="dark"] .switch input:checked + .track, | |
| @media (prefers-color-scheme: dark) { .switch input:checked + .track { background: var(--accent-dark); } } | |
| .switch .knob { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: var(--surface-2); transition: transform .15s; } | |
| .switch input:checked ~ .knob { transform: translateX(16px); } | |
| .run-btn { | |
| width: 100%; padding: 13px; border-radius: var(--radius); border: none; | |
| background: var(--accent-light); color: white; font-size: 14px; font-weight: 500; cursor: pointer; | |
| margin-top: 4px; | |
| } | |
| [data-mode="dark"] .run-btn { background: var(--accent-dark); color: #042C53; } | |
| @media (prefers-color-scheme: dark) { .run-btn { background: var(--accent-dark); color: #042C53; } } | |
| .run-btn:disabled { opacity: .5; } | |
| .status { font-size: 13px; color: var(--text-secondary); text-align: center; margin: 10px 0; min-height: 18px; } | |
| .results { margin: 14px 0 40px; } | |
| .results h3 { font-size: 14px; font-weight: 500; margin: 0 0 8px; } | |
| .panel-row { display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: var(--surface-1); | |
| border-radius: 8px; margin-bottom: 6px; font-size: 13px; } | |
| .swatch { width: 14px; height: 14px; border-radius: 3px; flex: 0 0 auto; } | |
| .panel-row .meta { color: var(--text-secondary); } | |
| .panel-row .meta b { color: var(--text-primary); font-weight: 500; } | |
| .empty-note { font-size: 13px; color: var(--text-muted); text-align: center; padding: 20px 0; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="wrap"> | |
| <h2>Comic panel detector</h2> | |
| <p class="sub">Finds panel boundaries by tracing the actual border lines β handles rectangles, diagonals, and irregular shapes, not just grid layouts.</p> | |
| <label class="upload-btn" for="fileInput"> | |
| <span id="uploadLabel">π€ Upload comic page(s)</span> | |
| </label> | |
| <input type="file" id="fileInput" accept="image/*" multiple> | |
| <div class="thumbs" id="thumbs" style="display:none"></div> | |
| <div class="canvas-card" id="canvasCard" style="display:none"> | |
| <div class="canvas-scroll"> | |
| <canvas id="canvas"></canvas> | |
| </div> | |
| </div> | |
| <div class="controls" id="controls" style="display:none"> | |
| <div class="ctrl-row"> | |
| <div class="ctrl-label"><span>Border darkness threshold</span><span id="threshVal">80</span></div> | |
| <input type="range" id="threshSlider" min="20" max="180" value="80"> | |
| </div> | |
| <div class="ctrl-row"> | |
| <div class="ctrl-label"><span>Minimum panel size (% of page)</span><span id="areaVal">1.5%</span></div> | |
| <input type="range" id="areaSlider" min="2" max="100" value="15"> | |
| </div> | |
| <div class="ctrl-row"> | |
| <div class="ctrl-label"><span>Line-gap bridging</span><span id="dilateVal">2</span></div> | |
| <input type="range" id="dilateSlider" min="0" max="8" value="2"> | |
| </div> | |
| <div class="ctrl-row toggle-row"> | |
| <div class="ctrl-label"><span>Exclude regions touching page edge</span></div> | |
| <label class="switch"> | |
| <input type="checkbox" id="edgeToggle" checked> | |
| <div class="track"></div> | |
| <div class="knob"></div> | |
| </label> | |
| </div> | |
| <button class="run-btn" id="runBtn" disabled>Detect panels</button> | |
| </div> | |
| <div class="status" id="status"></div> | |
| <div class="results" id="results"></div> | |
| </div> | |
| <script async src="https://docs.opencv.org/4.9.0/opencv.js" onerror="document.getElementById('status').textContent='Failed to load OpenCV.js β check your internet connection and reload.'"></script> | |
| <script> | |
| const PANEL_COLORS = ['#D85A30','#0F6E56','#534AB7','#993556','#185FA5','#BA7517','#639922','#712B13']; | |
| let cvReady = false; | |
| let images = []; | |
| let activeIndex = -1; | |
| let srcMat = null; | |
| const fileInput = document.getElementById('fileInput'); | |
| const uploadLabel = document.getElementById('uploadLabel'); | |
| const thumbs = document.getElementById('thumbs'); | |
| const canvasCard = document.getElementById('canvasCard'); | |
| const canvas = document.getElementById('canvas'); | |
| const controls = document.getElementById('controls'); | |
| const runBtn = document.getElementById('runBtn'); | |
| const statusEl = document.getElementById('status'); | |
| const resultsEl = document.getElementById('results'); | |
| const threshSlider = document.getElementById('threshSlider'); | |
| const areaSlider = document.getElementById('areaSlider'); | |
| const dilateSlider = document.getElementById('dilateSlider'); | |
| const edgeToggle = document.getElementById('edgeToggle'); | |
| const threshVal = document.getElementById('threshVal'); | |
| const areaVal = document.getElementById('areaVal'); | |
| const dilateVal = document.getElementById('dilateVal'); | |
| threshSlider.addEventListener('input', () => { threshVal.textContent = threshSlider.value; }); | |
| dilateSlider.addEventListener('input', () => { dilateVal.textContent = dilateSlider.value; }); | |
| areaSlider.addEventListener('input', () => { areaVal.textContent = (areaSlider.value / 10).toFixed(1) + '%'; }); | |
| function setStatus(msg) { statusEl.textContent = msg; } | |
| window.cv = window.cv || {}; | |
| let cvWaitElapsed = 0; | |
| const CV_LOAD_TIMEOUT_MS = 20000; | |
| const waitForCv = setInterval(() => { | |
| cvWaitElapsed += 150; | |
| if (window.cv && window.cv.Mat) { | |
| clearInterval(waitForCv); | |
| cv['onRuntimeInitialized'] = () => { | |
| cvReady = true; | |
| if (images.length) runBtn.disabled = false; | |
| setStatus(images.length ? 'Ready β tap Detect panels' : 'Ready β upload a page to begin'); | |
| }; | |
| if (cv.getBuildInformation) { | |
| cvReady = true; | |
| if (images.length) runBtn.disabled = false; | |
| setStatus(images.length ? 'Ready β tap Detect panels' : 'Ready β upload a page to begin'); | |
| } | |
| return; | |
| } | |
| if (cvWaitElapsed >= CV_LOAD_TIMEOUT_MS) { | |
| clearInterval(waitForCv); | |
| setStatus('OpenCV.js failed to load (timed out). Check your connection and reload the page.'); | |
| } | |
| }, 150); | |
| setStatus('Loading image engine...'); | |
| fileInput.addEventListener('change', (e) => { | |
| const files = Array.from(e.target.files || []); | |
| if (!files.length) return; | |
| images = []; | |
| thumbs.innerHTML = ''; | |
| thumbs.style.display = 'flex'; | |
| uploadLabel.textContent = files.length + ' image(s) loaded'; | |
| files.forEach((file, i) => { | |
| const reader = new FileReader(); | |
| reader.onload = (ev) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| images[i] = img; | |
| const thumb = document.createElement('div'); | |
| thumb.className = 'thumb'; | |
| thumb.innerHTML = '<img src="' + ev.target.result + '">'; | |
| thumb.addEventListener('click', () => selectImage(i)); | |
| thumbs.appendChild(thumb); | |
| if (activeIndex === -1) selectImage(i); | |
| }; | |
| img.src = ev.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| }); | |
| }); | |
| function selectImage(i) { | |
| activeIndex = i; | |
| Array.from(thumbs.children).forEach((t, idx) => t.classList.toggle('active', idx === i)); | |
| drawOriginal(images[i]); | |
| canvasCard.style.display = 'block'; | |
| controls.style.display = 'block'; | |
| resultsEl.innerHTML = ''; | |
| runBtn.disabled = !cvReady; | |
| setStatus(cvReady ? 'Ready β tap Detect panels' : 'Loading image engine...'); | |
| } | |
| function drawOriginal(img) { | |
| const maxDim = 1400; | |
| let w = img.naturalWidth, h = img.naturalHeight; | |
| if (Math.max(w, h) > maxDim) { | |
| const scale = maxDim / Math.max(w, h); | |
| w = Math.round(w * scale); | |
| h = Math.round(h * scale); | |
| } | |
| canvas.width = w; | |
| canvas.height = h; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(img, 0, 0, w, h); | |
| } | |
| runBtn.addEventListener('click', () => { | |
| if (!cvReady || activeIndex === -1) return; | |
| runBtn.disabled = true; | |
| setStatus('Detecting panels...'); | |
| setTimeout(detectPanels, 30); | |
| }); | |
| function detectPanels() { | |
| try { | |
| const ctx = canvas.getContext('2d'); | |
| drawOriginal(images[activeIndex]); | |
| const src = cv.imread(canvas); | |
| const gray = new cv.Mat(); | |
| cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); | |
| const darkThresh = parseInt(threshSlider.value, 10); | |
| const minAreaFrac = parseInt(areaSlider.value, 10) / 1000; | |
| const dilateIter = parseInt(dilateSlider.value, 10); | |
| const excludeEdge = edgeToggle.checked; | |
| const bw = new cv.Mat(); | |
| cv.threshold(gray, bw, darkThresh, 255, cv.THRESH_BINARY); | |
| const borderMask = new cv.Mat(); | |
| cv.bitwise_not(bw, borderMask); | |
| const kernel = cv.Mat.ones(3, 3, cv.CV_8U); | |
| const dilated = new cv.Mat(); | |
| if (dilateIter > 0) { | |
| cv.dilate(borderMask, dilated, kernel, new cv.Point(-1, -1), dilateIter); | |
| } else { | |
| borderMask.copyTo(dilated); | |
| } | |
| const nonBorder = new cv.Mat(); | |
| cv.bitwise_not(dilated, nonBorder); | |
| const contours = new cv.MatVector(); | |
| const hierarchy = new cv.Mat(); | |
| cv.findContours(nonBorder, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE); | |
| const imgArea = src.rows * src.cols; | |
| const panels = []; | |
| for (let i = 0; i < contours.size(); i++) { | |
| const cnt = contours.get(i); | |
| const area = cv.contourArea(cnt); | |
| if (area < imgArea * minAreaFrac) { cnt.delete(); continue; } | |
| const rect = cv.boundingRect(cnt); | |
| if (excludeEdge) { | |
| const m = 2; | |
| if (rect.x <= m || rect.y <= m || rect.x + rect.width >= src.cols - m || rect.y + rect.height >= src.rows - m) { | |
| cnt.delete(); | |
| continue; | |
| } | |
| } | |
| const approx = new cv.Mat(); | |
| const peri = cv.arcLength(cnt, true); | |
| cv.approxPolyDP(cnt, approx, 0.01 * peri, true); | |
| const points = []; | |
| for (let p = 0; p < approx.rows; p++) { | |
| points.push({ x: approx.data32S[p * 2], y: approx.data32S[p * 2 + 1] }); | |
| } | |
| approx.delete(); | |
| panels.push({ points, area, bbox: rect }); | |
| cnt.delete(); | |
| } | |
| src.delete(); gray.delete(); bw.delete(); borderMask.delete(); | |
| kernel.delete(); dilated.delete(); nonBorder.delete(); | |
| contours.delete(); hierarchy.delete(); | |
| panels.sort((a, b) => (a.bbox.y - b.bbox.y) || (a.bbox.x - b.bbox.x)); | |
| renderResults(panels, ctx); | |
| } catch (err) { | |
| setStatus('Detection failed: ' + err.message); | |
| } finally { | |
| runBtn.disabled = false; | |
| } | |
| } | |
| function renderResults(panels, ctx) { | |
| if (!panels.length) { | |
| setStatus('No panels found β try lowering the size threshold or adjusting darkness.'); | |
| resultsEl.innerHTML = '<div class="empty-note">No regions matched the current settings.</div>'; | |
| return; | |
| } | |
| panels.forEach((panel, idx) => { | |
| const color = PANEL_COLORS[idx % PANEL_COLORS.length]; | |
| ctx.strokeStyle = color; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| panel.points.forEach((pt, i) => { | |
| if (i === 0) ctx.moveTo(pt.x, pt.y); else ctx.lineTo(pt.x, pt.y); | |
| }); | |
| ctx.closePath(); | |
| ctx.stroke(); | |
| const cx = panel.points.reduce((s, p) => s + p.x, 0) / panel.points.length; | |
| const cy = panel.points.reduce((s, p) => s + p.y, 0) / panel.points.length; | |
| ctx.fillStyle = color; | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, 14, 0, Math.PI * 2); | |
| ctx.fill(); | |
| ctx.fillStyle = '#fff'; | |
| ctx.font = 'bold 16px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(String(idx), cx, cy); | |
| }); | |
| resultsEl.innerHTML = '<h3>' + panels.length + ' panel(s) found</h3>' + | |
| panels.map((p, idx) => { | |
| const color = PANEL_COLORS[idx % PANEL_COLORS.length]; | |
| const shape = p.points.length <= 4 ? (p.points.length === 4 ? 'rectangle' : p.points.length + '-sided') | |
| : p.points.length + '-point polygon (irregular)'; | |
| return '<div class="panel-row">' + | |
| '<div class="swatch" style="background:' + color + '"></div>' + | |
| '<div class="meta"><b>Panel ' + idx + '</b> β ' + shape + | |
| ' Β· bbox ' + p.bbox.width + 'Γ' + p.bbox.height + 'px</div>' + | |
| '</div>'; | |
| }).join(''); | |
| setStatus(panels.length + ' panel(s) detected β adjust sliders and re-run if this looks wrong.'); | |
| } | |
| </script> | |
| </body> | |
| </html> |