mathcaptchaforge-demo / index.html
emrecengdev's picture
Static in-browser demo for mathcaptchaforge-onnx
a47a569 verified
Raw History Blame Contribute Delete
9.62 kB
<!doctype html>
<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>