Spaces:
Running
Running
davanstrien HF Staff
Accept/reject feedback: per-tag votes stored in a private bucket with a pseudonymous id
5605c42 verified Download static/index.html from davanstrien/hub-task-tagger: direct link, hf CLI and curl.
- Browser
- Download file 14.6 kB
-
https://huggingface.co/spaces/davanstrien/hub-task-tagger/resolve/main/static/index.html
- Command line
-
hf download hf://spaces/davanstrien/hub-task-tagger/static/index.html
-
curl -L -o index.html https://huggingface.co/spaces/davanstrien/hub-task-tagger/resolve/main/static/index.html
14.6 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Dataset tagger</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet"> | |
| <style> | |
| :root{--bg:#F7F8FA;--ink:#1D2330;--muted:#6B7280;--rule:#E1E4EA;--bar:#3450C4;--bar-soft:#E3E7F6;--owner:#8A5F0E;--owner-soft:#F5EBD6;--panel:#EEF1F5} | |
| *{box-sizing:border-box} | |
| body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 "Schibsted Grotesk",system-ui,sans-serif} | |
| main{max-width:980px;margin:0 auto;padding:52px 24px 72px} | |
| h1{font-weight:700;font-size:36px;line-height:1.1;letter-spacing:-.015em;margin:0 0 8px} | |
| .lede{color:var(--muted);max-width:62ch;margin:0 0 26px} | |
| form{display:flex;gap:8px;max-width:680px;background:#fff;border:1px solid #C9CED8;border-radius:10px;padding:6px} | |
| input{flex:1;border:0;background:transparent;font:500 18px "Schibsted Grotesk",sans-serif;padding:8px 10px;color:var(--ink);min-width:0} | |
| input:focus{outline:none} | |
| form:focus-within{border-color:var(--bar);box-shadow:0 0 0 3px var(--bar-soft)} | |
| button{font:600 15px "Schibsted Grotesk",sans-serif;border:0;border-radius:7px;background:var(--ink);color:#fff;cursor:pointer;padding:0 18px} | |
| button:focus-visible,a:focus-visible{outline:2px solid var(--bar);outline-offset:3px} | |
| #go,button.secondary{border:1px solid var(--ink)}button.secondary{background:#fff;color:var(--ink)} | |
| .examples{margin:10px 0 42px;color:var(--muted);font-size:14px} | |
| .examples button{font:14px "Schibsted Grotesk",sans-serif;background:none;color:var(--ink);text-decoration:underline;text-decoration-color:#C9CED8;text-underline-offset:3px;margin-right:14px;padding:0;border-radius:0} | |
| .grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:44px;align-items:start} | |
| h2{font-size:14px;font-weight:600;margin:0 0 12px;color:var(--muted)} | |
| .dsname{font-size:21px;font-weight:600;margin:0 0 2px;word-break:break-all} | |
| .meta{font-size:14px;margin:0 0 24px} | |
| .meta a,.about a{color:var(--bar);text-underline-offset:3px} | |
| .row{display:grid;grid-template-columns:minmax(0,1fr) 130px 52px;align-items:center;gap:14px;padding:6px 0} | |
| .row .name{color:var(--muted)} | |
| .row.set .name{color:var(--ink);font-weight:600} | |
| .row .track{height:6px;background:var(--bar-soft);border-radius:3px;position:relative;overflow:hidden} | |
| .row .bar{position:absolute;inset:0 auto 0 0;background:var(--bar);border-radius:3px;opacity:.35} | |
| .row.set .bar{opacity:1} | |
| .row .p{text-align:right;font-variant-numeric:tabular-nums;color:var(--muted);font-size:14px} | |
| .row.set .p{color:var(--ink);font-weight:600} | |
| #tags.fb .row{grid-template-columns:minmax(0,1fr) 130px 52px 58px} | |
| .fbt{display:flex;gap:4px;justify-content:flex-end} | |
| .fbt button{font:14px/1 "Schibsted Grotesk",sans-serif;width:26px;height:26px;padding:0;background:none;color:var(--muted);border:1px solid var(--rule);border-radius:6px} | |
| .fbt button[aria-pressed=true].yes{color:var(--bar);border-color:var(--bar);background:var(--bar-soft)} | |
| .fbt button[aria-pressed=true].no{color:#B04A2F;border-color:#B04A2F;background:#F8E6E1} | |
| .fbbar{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin:12px 0 0;font-size:13px;color:var(--muted)}.fbbar p{margin:0}.fbbar[hidden]{display:none} | |
| .fbbar button{font-size:14px;padding:6px 14px} | |
| .tag-note{grid-column:1 / -1;font-size:13px;margin:-2px 0 4px;color:var(--muted)} | |
| .owner-mark{display:inline-block;background:var(--owner-soft);color:var(--owner);font-size:12px;font-weight:500;border-radius:999px;padding:1px 8px;margin-left:8px;vertical-align:1px} | |
| .owner{margin-top:20px;font-size:14px;color:var(--muted)} | |
| .owner small{font-size:12px;color:var(--muted)} | |
| .owner span{color:var(--owner);font-weight:500} | |
| .reads{font:13px/1.7 "JetBrains Mono",ui-monospace,monospace;white-space:pre-wrap;word-break:break-word;color:#2A3140;background:var(--panel);border-radius:10px;padding:16px 18px;max-height:420px;overflow:auto;margin:0} | |
| .caption{color:var(--muted);font-size:13px;margin:8px 0 0} | |
| .viewer{margin-top:48px} | |
| .viewer iframe{width:100%;height:520px;border:1px solid var(--rule);border-radius:10px;background:#fff} | |
| .about{margin-top:52px;max-width:64ch;font-size:15px;color:#3A4150;border-top:1px solid var(--rule);padding-top:20px}.about p{margin:0 0 10px}.todo{color:#B04A2F;font-style:italic} | |
| .timing{margin-top:18px;color:var(--muted);font-size:13px} | |
| .status{margin:-28px 0 42px;font-size:15px;max-width:680px}.status:empty{display:none}.status.err{color:#B04A2F}.status.load{color:var(--muted)}button:disabled{opacity:.6;cursor:default} | |
| @media (max-width:760px){.grid{grid-template-columns:1fr;gap:34px}.row{grid-template-columns:minmax(0,1fr) 80px 48px}#tags.fb .row{grid-template-columns:minmax(0,1fr) 56px 44px 58px;gap:10px}} | |
| @media (max-width:560px){#tags.fb .row{grid-template-columns:minmax(0,1fr) 36px 42px 58px}form{flex-wrap:wrap}input{flex:1 1 100%}form button{flex:1 1 100%;padding:11px 18px}} | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <h1>What is this dataset for?</h1> | |
| <p class="lede">Paste a Hugging Face dataset id. A small fine-tuned model reads the column names and the first row, then suggests task tags.</p> | |
| <form id="f"><input id="q" aria-label="Dataset id" placeholder="owner/dataset" autocomplete="off"><button type="button" id="random" class="secondary">Random dataset</button><button id="go">Suggest tags</button></form> | |
| <div class="examples">Examples: <span id="ex"></span></div> | |
| <p class="status" id="status" role="status" aria-live="polite"></p> | |
| <section id="out" hidden> | |
| <div class="grid"> | |
| <div> | |
| <p class="dsname" id="name"></p> | |
| <p class="meta"><a id="card" target="_blank" rel="noopener">Open the dataset card</a></p> | |
| <h2>Suggested tags</h2> | |
| <div id="tags"></div> | |
| <div class="fbbar" id="fbbar" hidden><button type="button" class="secondary" id="fbsend" hidden>Send feedback</button><p id="fbnote">Feedback is stored with a pseudonymous id to evaluate and improve the model.</p></div> | |
| <p class="owner">Owner's tags: <span id="owner"></span><br><small>Set by whoever uploaded the dataset, so often incomplete.</small></p> | |
| <p class="timing" id="timing"></p> | |
| </div> | |
| <div> | |
| <h2>What the model reads</h2> | |
| <pre class="reads" id="reads"></pre> | |
| <p class="caption">Column names and types, then the first row, cut to fit the model's input.</p> | |
| </div> | |
| </div> | |
| <div class="viewer"> | |
| <h2>First rows</h2> | |
| <iframe id="viewer" title="Dataset viewer" loading="lazy"></iframe> | |
| </div> | |
| </section> | |
| <section class="about"> | |
| <h2>About the model</h2> | |
| <p>TypeSafe AI's <a href="https://docs.typesafe.ai/introduction" target="_blank" rel="noopener">Jev</a> has got people excited about "System One" models: models that don't generate text, but read a state and return typed answers with probabilities that software can use directly. This page does the same thing for one narrow job. The state is a dataset's columns and first row. The answer is a set of task tags, each with a probability. The model is small enough to fine-tune yourself on Jobs and to run on a free CPU.</p> | |
| <p>The tags come from <a href="https://huggingface.co/davanstrien/hub-task-tagger-gliner2.5-base" target="_blank" rel="noopener">GLiNER2.5-base</a>, a 194M-parameter model from <a href="https://huggingface.co/fastino/gliner2.5-base-v1" target="_blank" rel="noopener">Fastino</a> that reads a text and a list of labels together. It was fine-tuned on 16,000 Hub datasets and their owners' tags, in 17 minutes on one GPU with <a href="https://huggingface.co/datasets/uv-scripts/classification/blob/main/train-gliner2.py" target="_blank" rel="noopener">this training script</a>.</p> | |
| <p>On 3,000 newer datasets from owners it never saw, its first suggestion matches an owner's tag 69% of the time. That is a noisy target: owners often leave out tags that fit, and in a hand-checked sample about one dataset in ten was missing one. A suggestion that isn't in the owner's tags is not always wrong.</p> | |
| </section> | |
| </main> | |
| <script> | |
| const EXAMPLES = ["hubxrt/LPMusicCapsMTT_a2t", "dougdotcon/douvras-failure-atlas", "mim-chess-vlas/train_800_dense__mask__overlay_a75__sim__agentview_camera__static"]; | |
| const $ = id => document.getElementById(id); | |
| function pct(p){ return p < 0.001 ? "<0.1%" : (p*100).toFixed(p < 0.1 ? 1 : 0) + "%"; } | |
| function status(text, kind){ $("status").textContent = text; $("status").className = "status " + (kind || ""); } | |
| function show(ex){ | |
| $("out").hidden = false; $("q").value = ex.dataset_id; | |
| $("name").textContent = ex.dataset_id; | |
| $("card").href = "https://huggingface.co/datasets/" + ex.dataset_id; | |
| const declared = new Set(ex.declared), inSet = new Set(ex.set); | |
| $("tags").innerHTML = ex.top.map(t => { | |
| const set = inSet.has(t.tag), own = declared.has(t.tag); | |
| const mark = own ? `<span class="owner-mark">owner's tag</span>` : ""; | |
| const note = set && !own ? `<p class="tag-note">Not in the owner's tags.</p>` : ""; | |
| const toggles = ex.feedback ? `<span class="fbt"><button type="button" class="yes" data-tag="${t.tag}" aria-pressed="false" aria-label="${t.tag} fits" title="Fits">✓</button><button type="button" class="no" data-tag="${t.tag}" aria-pressed="false" aria-label="${t.tag} does not fit" title="Does not fit">✗</button></span>` : ""; | |
| return `<div class="row ${set ? "set" : ""}"><span class="name">${t.tag}${mark}</span><span class="track"><span class="bar" style="width:${Math.max(t.p*100,0.8)}%"></span></span><span class="p">${pct(t.p)}</span>${toggles}${note}</div>`; | |
| }).join(""); | |
| $("tags").classList.toggle("fb", !!ex.feedback); | |
| current = ex; votes = {}; sent = false; | |
| $("fbbar").hidden = !ex.feedback; $("fbsend").hidden = true; $("fbsend").disabled = false; | |
| $("fbnote").textContent = "Feedback is stored with a pseudonymous id to evaluate and improve the model."; | |
| $("owner").textContent = ex.declared.length ? ex.declared.join(", ") : "none"; | |
| $("reads").textContent = ex.state; | |
| const viewer = `https://huggingface.co/datasets/${ex.dataset_id}/embed/viewer`; | |
| if ($("viewer").src !== viewer) $("viewer").src = viewer; | |
| $("timing").textContent = ex.timing.cached ? "Served from cache." | |
| : `Fetched the preview in ${ex.timing.fetch_ms} ms. The model took ${ex.timing.model_ms} ms on a free CPU.`; | |
| } | |
| let pending = null; | |
| // Feedback on the shown result: votes[tag] = "yes" | "no"; one submission per shown result. | |
| let current = null, votes = {}, sent = false; | |
| $("tags").onclick = e => { | |
| const b = e.target.closest(".fbt button"); | |
| if (!b || sent) return; | |
| const tag = b.dataset.tag, v = b.classList.contains("yes") ? "yes" : "no"; | |
| votes[tag] = votes[tag] === v ? undefined : v; | |
| for (const x of $("tags").querySelectorAll(`.fbt button[data-tag="${tag}"]`)) | |
| x.setAttribute("aria-pressed", String(votes[tag] === (x.classList.contains("yes") ? "yes" : "no"))); | |
| $("fbsend").hidden = !Object.values(votes).some(Boolean); | |
| }; | |
| $("fbsend").onclick = async () => { | |
| if (sent || !current) return; | |
| const tags = Object.keys(votes); | |
| const body = { | |
| dataset_id: current.dataset_id, sha: current.sha, | |
| model_repo: current.model.repo, model_revision: current.model.revision, T: current.model.T, tau: current.model.tau, | |
| suggested: current.top, set: current.set, | |
| accepted: tags.filter(t => votes[t] === "yes"), rejected: tags.filter(t => votes[t] === "no"), | |
| owner_tags: current.declared, ts: new Date().toISOString(), | |
| }; | |
| $("fbsend").disabled = true; | |
| try { | |
| const r = await fetch("api/feedback", {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)}); | |
| let reply = null; | |
| try { reply = await r.json(); } catch (_) {} | |
| if (!r.ok) { | |
| $("fbnote").textContent = reply && reply.message ? reply.message : `Could not send the feedback (status ${r.status}).`; | |
| $("fbsend").disabled = r.status === 400; // a retry cannot fix invalid feedback | |
| return; | |
| } | |
| sent = true; | |
| $("fbsend").hidden = true; | |
| for (const x of $("tags").querySelectorAll(".fbt button")) x.disabled = true; | |
| $("fbnote").textContent = "Thanks. Your feedback was saved."; | |
| } catch (e) { | |
| $("fbnote").textContent = "Could not reach the server. Try again."; | |
| $("fbsend").disabled = false; | |
| } | |
| }; | |
| function busy(on){ $("go").disabled = on; $("random").disabled = on; } | |
| async function run(id){ | |
| id = id.trim(); | |
| if (!id) { $("out").hidden = true; status("Paste a dataset id first, for example " + EXAMPLES[0] + ".", "err"); return; } | |
| if (pending) pending.abort(); | |
| pending = new AbortController(); | |
| busy(true); $("out").hidden = true; | |
| status("Reading the dataset…", "load"); | |
| try { | |
| const r = await fetch("api/predict", {method: "POST", headers: {"Content-Type": "application/json"}, | |
| body: JSON.stringify({dataset_id: id}), signal: pending.signal}); | |
| let body = null; | |
| try { body = await r.json(); } catch (_) {} | |
| if (!r.ok) { | |
| status(body && body.message ? body.message | |
| : `Something went wrong on the server (status ${r.status}). Try again in a minute.`, "err"); | |
| return; | |
| } | |
| status(""); | |
| show(body); | |
| } catch (e) { | |
| if (e.name === "AbortError") return; | |
| status("Could not reach the server. Check your connection and try again.", "err"); | |
| } finally { | |
| busy(false); | |
| } | |
| } | |
| $("ex").innerHTML = EXAMPLES.map((id,i) => `<button type="button" data-i="${i}">${id.length > 40 ? id.slice(0,38) + "…" : id}</button>`).join(""); | |
| $("ex").onclick = e => { const i = e.target.dataset.i; if (i !== undefined) { $("q").value = EXAMPLES[i]; run(EXAMPLES[i]); } }; | |
| $("random").onclick = async () => { | |
| if (pending) pending.abort(); | |
| busy(true); $("out").hidden = true; | |
| status("Picking a random dataset…", "load"); | |
| try { | |
| const r = await fetch("api/random"); | |
| let body = null; | |
| try { body = await r.json(); } catch (_) {} | |
| if (!r.ok) { | |
| status(body && body.message ? body.message | |
| : `Could not pick a random dataset (status ${r.status}). Try one of the examples.`, "err"); | |
| return; | |
| } | |
| $("q").value = body.dataset_id; | |
| await run(body.dataset_id); // run() keeps both buttons disabled until the prediction arrives | |
| } catch (e) { | |
| status("Could not reach the server. Check your connection and try again.", "err"); | |
| } finally { | |
| busy(false); | |
| } | |
| }; | |
| $("f").onsubmit = e => { e.preventDefault(); run($("q").value); }; | |
| </script> | |
| </body> | |
| </html> |