File size: 14,578 Bytes
f3e5f3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aec6ff1
f3e5f3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5605c42
 
 
 
 
 
 
f3e5f3c
 
 
 
 
 
 
 
 
 
 
a14bc39
5605c42
 
f3e5f3c
 
 
 
 
 
aec6ff1
 
f3e5f3c
 
 
 
 
 
 
 
 
5605c42
f3e5f3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1dad8a8
f3e5f3c
 
 
 
 
 
 
 
 
 
a14bc39
f3e5f3c
 
 
 
 
 
 
5605c42
 
f3e5f3c
5605c42
 
 
 
f3e5f3c
 
 
 
2a8957e
 
f3e5f3c
 
5605c42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aec6ff1
f3e5f3c
 
a14bc39
f3e5f3c
 
aec6ff1
f3e5f3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aec6ff1
f3e5f3c
 
 
 
a6d466b
 
aec6ff1
a6d466b
 
 
 
 
 
 
 
 
 
 
aec6ff1
a6d466b
 
 
aec6ff1
a6d466b
 
f3e5f3c
 
 
 
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
<!doctype html>
<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>