File size: 9,619 Bytes
e081002 a47a569 e081002 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | <!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>
|