Download index.html from emrecengdev/mathcaptchaforge-demo: direct link, hf CLI and curl.
- Browser
- Download file 9.62 kB
-
https://huggingface.co/spaces/emrecengdev/mathcaptchaforge-demo/resolve/main/index.html
- Command line
-
hf download hf://spaces/emrecengdev/mathcaptchaforge-demo/index.html
-
curl -L -o index.html https://huggingface.co/spaces/emrecengdev/mathcaptchaforge-demo/resolve/main/index.html
9.62 kB
| <html lang="tr"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>MathCaptchaForge</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,800&family=Caveat:wght@700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --paper: #F7F9FC; | |
| --grid: #DCE6F2; | |
| --ink: #1B2F6B; | |
| --pencil: #5A6478; | |
| --redpen: #C21F32; | |
| --line: #B9C7DB; | |
| } | |
| * { box-sizing: border-box; } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: "Bricolage Grotesque", system-ui, sans-serif; | |
| color: var(--ink); | |
| background-color: var(--paper); | |
| background-image: | |
| linear-gradient(var(--grid) 1px, transparent 1px), | |
| linear-gradient(90deg, var(--grid) 1px, transparent 1px); | |
| background-size: 24px 24px; | |
| } | |
| main { max-width: 960px; margin: 0 auto; padding: 56px 16px 48px; } | |
| h1 { font-size: clamp(2rem, 5vw, 3.25rem); font-weight: 800; letter-spacing: -0.02em; line-height: 1.05; margin: 0 0 12px; } | |
| .lede { color: var(--pencil); font-size: 1.0625rem; line-height: 1.55; max-width: 60ch; margin: 0 0 40px; } | |
| .sheet { display: grid; grid-template-columns: 1fr 1fr; gap: 0; background: #fff; border: 1px solid var(--line); border-radius: 6px; } | |
| .left { padding: 24px; border-right: 1px dashed var(--line); } | |
| .right { padding: 24px 28px; display: flex; flex-direction: column; } | |
| h2 { font-size: 1rem; font-weight: 600; margin: 0 0 12px; } | |
| .drop { | |
| display: grid; place-items: center; width: 100%; aspect-ratio: 160 / 64; | |
| border: 2px dashed var(--line); border-radius: 4px; background: var(--paper); | |
| cursor: pointer; color: var(--pencil); font: inherit; padding: 0; overflow: hidden; | |
| } | |
| .drop:hover, .drop.over { border-color: var(--ink); } | |
| .drop:focus-visible, .ex:focus-visible { outline: 3px solid var(--ink); outline-offset: 2px; } | |
| .drop img { width: 100%; height: 100%; object-fit: contain; image-rendering: pixelated; } | |
| .hint { font-size: .875rem; color: var(--pencil); margin: 8px 0 24px; } | |
| .examples { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; } | |
| .ex { padding: 4px; border: 1px solid var(--line); background: #fff; border-radius: 3px; cursor: pointer; } | |
| .ex:hover { border-color: var(--ink); } | |
| .ex img { display: block; width: 100%; image-rendering: pixelated; } | |
| .answer { font-family: "Caveat", cursive; color: var(--redpen); font-size: clamp(5rem, 14vw, 8.5rem); line-height: .9; min-height: 1em; margin: 4px 0 8px; transform: rotate(-3deg); transform-origin: left; } | |
| .answer.empty { color: var(--line); } | |
| .answer.pop { animation: pop .35s cubic-bezier(.2,1.4,.4,1); } | |
| @keyframes pop { from { transform: rotate(-3deg) scale(.7); opacity: 0; } to { transform: rotate(-3deg) scale(1); opacity: 1; } } | |
| .status { font-size: .9375rem; color: var(--pencil); margin: 0 0 20px; min-height: 1.5em; } | |
| .status.warn { color: var(--redpen); } | |
| ol.top { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; } | |
| ol.top li { display: grid; grid-template-columns: 3ch 1fr 6ch; align-items: center; gap: 10px; font-variant-numeric: tabular-nums; font-size: .9375rem; } | |
| .bar { height: 8px; background: var(--grid); border-radius: 4px; overflow: hidden; } | |
| .bar span { display: block; height: 100%; background: var(--ink); transition: width .3s ease; } | |
| ol.top li:first-child .bar span { background: var(--redpen); } | |
| .pct { text-align: right; color: var(--pencil); } | |
| footer { margin-top: 28px; font-size: .875rem; color: var(--pencil); line-height: 1.6; } | |
| footer a { color: var(--ink); } | |
| @media (max-width: 720px) { | |
| main { padding-top: 32px; } | |
| .sheet { grid-template-columns: 1fr; } | |
| .left { border-right: 0; border-bottom: 1px dashed var(--line); } | |
| } | |
| @media (prefers-reduced-motion: reduce) { .answer.pop { animation: none; } .bar span { transition: none; } } | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <h1>MathCaptchaForge</h1> | |
| <p class="lede">Bir matematik captcha'sı yükle; model işlemi okumadan sonucu doğrudan tahmin eder. Her şey tarayıcında çalışır, görsel hiçbir yere gönderilmez.</p> | |
| <div class="sheet"> | |
| <section class="left"> | |
| <h2>Captcha</h2> | |
| <button class="drop" id="drop" type="button" aria-label="Captcha görseli seç">Görseli buraya bırak ya da tıkla</button> | |
| <input type="file" id="file" accept="image/*" hidden> | |
| <p class="hint">Yapıştırmak için Ctrl+V de çalışır.</p> | |
| <h2>Test setinden örnekler</h2> | |
| <div class="examples" id="examples"></div> | |
| </section> | |
| <section class="right" aria-live="polite"> | |
| <h2>Cevap</h2> | |
| <div class="answer empty" id="answer">?</div> | |
| <p class="status" id="status">Model yükleniyor…</p> | |
| <ol class="top" id="top"></ol> | |
| </section> | |
| </div> | |
| <footer> | |
| Model: <a href="https://huggingface.co/emrecengdev/mathcaptchaforge-onnx">emrecengdev/mathcaptchaforge-onnx</a>. MobileNetV3, 103 sınıf (0–105), test doğruluğu %96,1. | |
| Güven %60'ın ya da ilk iki tahmin arasındaki fark 10 puanın altındaysa sonuç düşük güvenli sayılır. | |
| </footer> | |
| </main> | |
| <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/ort.min.js"></script> | |
| <script> | |
| const REPO = "https://huggingface.co/emrecengdev/mathcaptchaforge-onnx/resolve/main/"; | |
| const W = 160, H = 64, MEAN = [0.485, 0.456, 0.406], STD = [0.229, 0.224, 0.225]; | |
| const MIN_CONF = 0.6, MIN_MARGIN = 0.1; | |
| const EXAMPLES = ["00_31", "01_45", "02_41", "03_73", "04_26", "05_58", "06_40", "07_43"]; | |
| const $ = id => document.getElementById(id); | |
| let session, idxToClass, pending; | |
| ort.env.wasm.wasmPaths = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/"; | |
| async function init() { | |
| try { | |
| const [s, map] = await Promise.all([ | |
| ort.InferenceSession.create(REPO + "model_fp32.onnx", { executionProviders: ["wasm"] }), | |
| fetch(REPO + "class_map.json").then(r => r.json()), | |
| ]); | |
| session = s; idxToClass = map.idx_to_class; | |
| $("status").textContent = "Hazır. Bir görsel seç ya da örneklerden birine tıkla."; | |
| if (pending) run(pending); | |
| } catch (e) { | |
| $("status").textContent = "Model yüklenemedi: " + e.message + ". Sayfayı yenile."; | |
| $("status").className = "status warn"; | |
| } | |
| } | |
| function preprocess(img) { | |
| const c = document.createElement("canvas"); | |
| c.width = W; c.height = H; | |
| const ctx = c.getContext("2d"); | |
| ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, W, H); | |
| const s = Math.min(W / img.naturalWidth, H / img.naturalHeight); | |
| const rw = Math.max(1, Math.round(img.naturalWidth * s)), rh = Math.max(1, Math.round(img.naturalHeight * s)); | |
| ctx.imageSmoothingEnabled = true; | |
| ctx.drawImage(img, Math.floor((W - rw) / 2), Math.floor((H - rh) / 2), rw, rh); | |
| const px = ctx.getImageData(0, 0, W, H).data; | |
| const out = new Float32Array(3 * W * H); | |
| for (let i = 0; i < W * H; i++) { | |
| for (let ch = 0; ch < 3; ch++) out[ch * W * H + i] = (px[i * 4 + ch] / 255 - MEAN[ch]) / STD[ch]; | |
| } | |
| return new ort.Tensor("float32", out, [1, 3, H, W]); | |
| } | |
| async function run(img) { | |
| if (!session) { pending = img; $("status").textContent = "Model yükleniyor, birazdan çözülecek…"; return; } | |
| const t0 = performance.now(); | |
| const logits = (await session.run({ input: preprocess(img) })).logits.data; | |
| const ms = performance.now() - t0; | |
| const max = Math.max(...logits); | |
| const exps = Array.from(logits, v => Math.exp(v - max)); | |
| const sum = exps.reduce((a, b) => a + b, 0); | |
| const ranked = exps.map((v, i) => [i, v / sum]).sort((a, b) => b[1] - a[1]).slice(0, 5); | |
| const [conf, second] = [ranked[0][1], ranked[1][1]]; | |
| const ok = conf >= MIN_CONF && conf - second >= MIN_MARGIN; | |
| const a = $("answer"); | |
| a.textContent = idxToClass[ranked[0][0]]; | |
| a.className = "answer"; | |
| void a.offsetWidth; a.classList.add("pop"); | |
| const st = $("status"); | |
| st.className = ok ? "status" : "status warn"; | |
| st.textContent = (ok ? "Güven %" : "Düşük güven: %") + (conf * 100).toFixed(1) + ", " + ms.toFixed(0) + " ms"; | |
| $("top").innerHTML = ranked.map(([i, p]) => | |
| `<li><strong>${idxToClass[i]}</strong><span class="bar"><span style="width:${(p * 100).toFixed(1)}%"></span></span><span class="pct">%${(p * 100).toFixed(1)}</span></li>` | |
| ).join(""); | |
| } | |
| function loadUrl(url) { | |
| const img = new Image(); | |
| img.onload = () => { | |
| const shown = new Image(); shown.src = url; shown.alt = "Seçilen captcha"; | |
| $("drop").replaceChildren(shown); | |
| run(img); | |
| }; | |
| img.src = url; | |
| } | |
| const loadFile = f => f && f.type.startsWith("image/") && loadUrl(URL.createObjectURL(f)); | |
| const exWrap = $("examples"); | |
| EXAMPLES.forEach(name => { | |
| const b = document.createElement("button"); | |
| b.type = "button"; b.className = "ex"; b.setAttribute("aria-label", "Örnek captcha"); | |
| b.innerHTML = `<img src="${name}.png" alt="">`; | |
| b.onclick = () => loadUrl(name + ".png"); | |
| exWrap.appendChild(b); | |
| }); | |
| const drop = $("drop"); | |
| drop.onclick = () => $("file").click(); | |
| $("file").onchange = e => loadFile(e.target.files[0]); | |
| drop.ondragover = e => { e.preventDefault(); drop.classList.add("over"); }; | |
| drop.ondragleave = () => drop.classList.remove("over"); | |
| drop.ondrop = e => { e.preventDefault(); drop.classList.remove("over"); loadFile(e.dataTransfer.files[0]); }; | |
| document.addEventListener("paste", e => { | |
| const item = [...e.clipboardData.items].find(i => i.type.startsWith("image/")); | |
| if (item) loadFile(item.getAsFile()); | |
| }); | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |