Upload folder using huggingface_hub
Browse files
README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: moodring
|
| 3 |
+
emoji: "💍"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
# moodring — in-browser emotion classification
|
| 11 |
+
|
| 12 |
+
Static demo of [UsmarHaider/moodring](https://github.com/UsmarHaider/moodring):
|
| 13 |
+
TF-IDF + one-vs-rest logistic regression over the 28-label GoEmotions taxonomy,
|
| 14 |
+
exported to a 3.8 MB binary and evaluated in the browser. "Try a sample" fetches
|
| 15 |
+
the held-out test split from the google-research GitHub repo at view time.
|
index.html
CHANGED
|
@@ -1,19 +1,198 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>moodring — emotion classification</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #fcfcfb; --card: #ffffff; --ink: #2b2b2a; --muted: #6e6e6b;
|
| 10 |
+
--line: #e4e4e1; --blue: #2a78d6; --orange: #eb6834;
|
| 11 |
+
--blue-soft: #e8f0fb; --track: #f0f0ee;
|
| 12 |
+
}
|
| 13 |
+
@media (prefers-color-scheme: dark) {
|
| 14 |
+
:root {
|
| 15 |
+
--bg: #1a1a19; --card: #232322; --ink: #e8e8e5; --muted: #9c9c98;
|
| 16 |
+
--line: #3a3a38; --blue: #3987e5; --orange: #d95926;
|
| 17 |
+
--blue-soft: #1e2f44; --track: #2e2e2c;
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
* { box-sizing: border-box; margin: 0; }
|
| 21 |
+
body {
|
| 22 |
+
background: var(--bg); color: var(--ink);
|
| 23 |
+
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 24 |
+
display: flex; justify-content: center; padding: 28px 16px;
|
| 25 |
+
}
|
| 26 |
+
main { width: 100%; max-width: 780px; }
|
| 27 |
+
header { display: flex; align-items: baseline; gap: 12px; margin-bottom: 4px; }
|
| 28 |
+
h1 { font-size: 22px; letter-spacing: -0.02em; }
|
| 29 |
+
h1 .ring { color: var(--blue); }
|
| 30 |
+
.tag { color: var(--muted); font-size: 13px; }
|
| 31 |
+
p.sub { color: var(--muted); font-size: 13.5px; margin-bottom: 18px; }
|
| 32 |
+
.card {
|
| 33 |
+
background: var(--card); border: 1px solid var(--line); border-radius: 10px;
|
| 34 |
+
padding: 16px; margin-bottom: 14px;
|
| 35 |
+
}
|
| 36 |
+
textarea {
|
| 37 |
+
width: 100%; min-height: 84px; resize: vertical; border: 1px solid var(--line);
|
| 38 |
+
border-radius: 8px; padding: 10px 12px; font: inherit; color: var(--ink);
|
| 39 |
+
background: var(--bg); outline: none;
|
| 40 |
+
}
|
| 41 |
+
textarea:focus { border-color: var(--blue); }
|
| 42 |
+
.row { display: flex; gap: 10px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
|
| 43 |
+
button {
|
| 44 |
+
font: inherit; font-size: 14px; font-weight: 600; border-radius: 8px;
|
| 45 |
+
padding: 8px 18px; cursor: pointer; border: 1px solid transparent;
|
| 46 |
+
}
|
| 47 |
+
#analyze { background: var(--blue); color: #fff; }
|
| 48 |
+
#analyze:disabled { opacity: 0.5; cursor: default; }
|
| 49 |
+
#sampleBtn { background: none; border-color: var(--line); color: var(--ink); }
|
| 50 |
+
#sampleBtn:hover { border-color: var(--blue); }
|
| 51 |
+
.meta { color: var(--muted); font-size: 12.5px; margin-left: auto; }
|
| 52 |
+
#result { display: none; }
|
| 53 |
+
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
|
| 54 |
+
.chip {
|
| 55 |
+
background: var(--blue-soft); color: var(--blue); border-radius: 999px;
|
| 56 |
+
font-size: 13px; font-weight: 650; padding: 3px 12px;
|
| 57 |
+
}
|
| 58 |
+
.chip.gold { background: none; border: 1px dashed var(--line); color: var(--muted); font-weight: 500; }
|
| 59 |
+
.note { color: var(--muted); font-size: 12.5px; margin-bottom: 12px; }
|
| 60 |
+
.note .warn { color: var(--orange); font-weight: 600; }
|
| 61 |
+
.bars { display: grid; grid-template-columns: 110px 1fr 46px; gap: 6px 10px; align-items: center; }
|
| 62 |
+
.bl { font-size: 13px; color: var(--ink); text-align: right; }
|
| 63 |
+
.bl.dim { color: var(--muted); }
|
| 64 |
+
.track { position: relative; height: 12px; background: var(--track); border-radius: 4px; }
|
| 65 |
+
.fill { position: absolute; inset: 0 auto 0 0; border-radius: 4px; background: var(--blue); min-width: 2px; }
|
| 66 |
+
.fill.dim { opacity: 0.35; }
|
| 67 |
+
.tick { position: absolute; top: -2px; bottom: -2px; width: 2px; background: var(--orange); border-radius: 1px; }
|
| 68 |
+
.bv { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
| 69 |
+
.legend { display: flex; gap: 16px; margin-top: 12px; color: var(--muted); font-size: 12px; align-items: center; }
|
| 70 |
+
.sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; background: var(--blue); margin-right: 5px; vertical-align: -1px; }
|
| 71 |
+
.swt { display: inline-block; width: 2px; height: 12px; background: var(--orange); margin-right: 6px; vertical-align: -2px; }
|
| 72 |
+
footer { color: var(--muted); font-size: 12px; margin-top: 4px; }
|
| 73 |
+
footer a { color: var(--blue); text-decoration: none; }
|
| 74 |
+
</style>
|
| 75 |
+
</head>
|
| 76 |
+
<body>
|
| 77 |
+
<script src="./shim.js"></script>
|
| 78 |
+
<main>
|
| 79 |
+
<header><h1>mood<span class="ring">ring</span></h1><span class="tag">multi-label emotion classification</span></header>
|
| 80 |
+
<p class="sub">28 emotions from the GoEmotions taxonomy, scored by a 30k-feature linear model.
|
| 81 |
+
Type something, or pull a real Reddit comment from the held-out test split.</p>
|
| 82 |
+
|
| 83 |
+
<div class="card">
|
| 84 |
+
<textarea id="text" placeholder="Type a sentence — e.g. “Thanks so much, this made my whole week!”"></textarea>
|
| 85 |
+
<div class="row">
|
| 86 |
+
<button id="analyze">Analyze</button>
|
| 87 |
+
<button id="sampleBtn">Try a sample</button>
|
| 88 |
+
<span class="meta" id="meta"></span>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<div class="card" id="result">
|
| 93 |
+
<div class="chips" id="chips"></div>
|
| 94 |
+
<div class="note" id="note"></div>
|
| 95 |
+
<div class="bars" id="bars"></div>
|
| 96 |
+
<div class="legend">
|
| 97 |
+
<span><span class="sw"></span>model score</span>
|
| 98 |
+
<span><span class="swt"></span>decision threshold (per label, tuned on dev)</span>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
<footer>TF-IDF (1–2 grams) + one-vs-rest logistic regression · macro-F1 0.44 on the GoEmotions test split ·
|
| 103 |
+
<a href="https://github.com/UsmarHaider/moodring">source</a></footer>
|
| 104 |
+
</main>
|
| 105 |
+
<script>
|
| 106 |
+
const $ = id => document.getElementById(id);
|
| 107 |
+
let gold = null, totalSamples = 5427;
|
| 108 |
+
|
| 109 |
+
function setBusy(b) { $('analyze').disabled = b; }
|
| 110 |
+
|
| 111 |
+
async function analyze() {
|
| 112 |
+
const text = $('text').value.trim();
|
| 113 |
+
if (!text) return;
|
| 114 |
+
setBusy(true);
|
| 115 |
+
const t0 = performance.now();
|
| 116 |
+
try {
|
| 117 |
+
const res = await fetch('/predict', {
|
| 118 |
+
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 119 |
+
body: JSON.stringify({text}),
|
| 120 |
+
});
|
| 121 |
+
if (!res.ok) throw new Error('predict failed: ' + res.status);
|
| 122 |
+
render(await res.json(), performance.now() - t0);
|
| 123 |
+
} catch (e) {
|
| 124 |
+
$('meta').textContent = e.message;
|
| 125 |
+
} finally { setBusy(false); }
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
function render(out, ms) {
|
| 129 |
+
const chips = $('chips');
|
| 130 |
+
chips.innerHTML = '';
|
| 131 |
+
for (const label of out.labels) {
|
| 132 |
+
const c = document.createElement('span');
|
| 133 |
+
c.className = 'chip'; c.textContent = label; chips.appendChild(c);
|
| 134 |
+
}
|
| 135 |
+
if (gold) {
|
| 136 |
+
for (const g of gold) {
|
| 137 |
+
const c = document.createElement('span');
|
| 138 |
+
c.className = 'chip gold'; c.textContent = 'gold: ' + g; chips.appendChild(c);
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
$('note').innerHTML = out.fallback
|
| 142 |
+
? '<span class="warn">no label cleared its threshold</span> — showing the top-scoring emotion instead'
|
| 143 |
+
: 'labels whose score cleared their per-label threshold';
|
| 144 |
+
|
| 145 |
+
const entries = Object.entries(out.scores).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
| 146 |
+
const bars = $('bars');
|
| 147 |
+
bars.innerHTML = '';
|
| 148 |
+
const max = Math.max(entries[0][1], ...entries.map(e => out.thresholds[e[0]]), 0.55);
|
| 149 |
+
for (const [label, score] of entries) {
|
| 150 |
+
const picked = out.labels.includes(label);
|
| 151 |
+
const bl = document.createElement('div');
|
| 152 |
+
bl.className = 'bl' + (picked ? '' : ' dim'); bl.textContent = label;
|
| 153 |
+
const track = document.createElement('div');
|
| 154 |
+
track.className = 'track';
|
| 155 |
+
const fill = document.createElement('div');
|
| 156 |
+
fill.className = 'fill' + (picked ? '' : ' dim');
|
| 157 |
+
fill.style.width = (100 * score / max) + '%';
|
| 158 |
+
const tick = document.createElement('div');
|
| 159 |
+
tick.className = 'tick';
|
| 160 |
+
tick.style.left = 'calc(' + (100 * out.thresholds[label] / max) + '% - 1px)';
|
| 161 |
+
track.append(fill, tick);
|
| 162 |
+
const bv = document.createElement('div');
|
| 163 |
+
bv.className = 'bv'; bv.textContent = score.toFixed(2);
|
| 164 |
+
bars.append(bl, track, bv);
|
| 165 |
+
}
|
| 166 |
+
$('result').style.display = 'block';
|
| 167 |
+
$('meta').textContent = ms.toFixed(0) + ' ms';
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
async function loadSample(index) {
|
| 171 |
+
const i = index ?? Math.floor(Math.random() * totalSamples);
|
| 172 |
+
try {
|
| 173 |
+
const res = await fetch('/sample?index=' + i);
|
| 174 |
+
if (!res.ok) throw new Error('samples unavailable on this host');
|
| 175 |
+
const s = await res.json();
|
| 176 |
+
totalSamples = s.total;
|
| 177 |
+
$('text').value = s.text;
|
| 178 |
+
gold = s.gold;
|
| 179 |
+
await analyze();
|
| 180 |
+
$('meta').textContent += ' · test example #' + s.index;
|
| 181 |
+
} catch (e) { $('meta').textContent = e.message; }
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
$('analyze').addEventListener('click', () => { gold = null; analyze(); });
|
| 185 |
+
$('sampleBtn').addEventListener('click', () => loadSample());
|
| 186 |
+
$('text').addEventListener('keydown', e => {
|
| 187 |
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { gold = null; analyze(); }
|
| 188 |
+
});
|
| 189 |
+
|
| 190 |
+
const params = new URLSearchParams(location.search);
|
| 191 |
+
if (params.get('demo')) {
|
| 192 |
+
const idx = params.get('sample');
|
| 193 |
+
if (idx !== null) loadSample(parseInt(idx, 10));
|
| 194 |
+
else { $('text').value = 'Thanks so much, this made my whole week!'; analyze(); }
|
| 195 |
+
}
|
| 196 |
+
</script>
|
| 197 |
+
</body>
|
| 198 |
</html>
|
model.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7290296b53da8858b0b02201b52b59d21e61be02764e764644a0b243fd701def
|
| 3 |
+
size 3823860
|
shim.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// JS runtime for the exported moodring model. Mirrors the sklearn pipeline:
|
| 2 |
+
// tokenize (runs of >=2 word chars, lowercased), unigrams+bigrams, sublinear
|
| 3 |
+
// tf, idf, l2 norm, then per-label logistic scores. Works in browser and Node.
|
| 4 |
+
|
| 5 |
+
const TOKEN_RE = /[\p{L}\p{N}_]{2,}/gu;
|
| 6 |
+
|
| 7 |
+
function parseModel(buffer) {
|
| 8 |
+
const bytes = new Uint8Array(buffer);
|
| 9 |
+
const magic = new TextDecoder().decode(bytes.subarray(0, 9));
|
| 10 |
+
if (magic !== "MOODRING1") throw new Error("bad model file");
|
| 11 |
+
const jsonLen = new DataView(buffer).getUint32(9, true);
|
| 12 |
+
const meta = JSON.parse(new TextDecoder().decode(bytes.subarray(13, 13 + jsonLen)));
|
| 13 |
+
const n = meta.n_features;
|
| 14 |
+
let off = 13 + jsonLen;
|
| 15 |
+
const idf = new Float32Array(buffer.slice(off, off + 4 * n));
|
| 16 |
+
off += 4 * n;
|
| 17 |
+
const coef = new Float32Array(buffer.slice(off, off + 4 * n * meta.emotions.length));
|
| 18 |
+
const vocab = new Map(meta.terms.map((t, i) => [t, i]));
|
| 19 |
+
return { meta, idf, coef, vocab, n };
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function tokenize(text) {
|
| 23 |
+
return (text.toLowerCase().match(TOKEN_RE) || []);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function vectorize(model, text) {
|
| 27 |
+
const tokens = tokenize(text);
|
| 28 |
+
const grams = tokens.slice();
|
| 29 |
+
for (let i = 0; i + 1 < tokens.length; i++) grams.push(tokens[i] + " " + tokens[i + 1]);
|
| 30 |
+
const counts = new Map();
|
| 31 |
+
for (const g of grams) {
|
| 32 |
+
const idx = model.vocab.get(g);
|
| 33 |
+
if (idx !== undefined) counts.set(idx, (counts.get(idx) || 0) + 1);
|
| 34 |
+
}
|
| 35 |
+
let normSq = 0;
|
| 36 |
+
const entries = [];
|
| 37 |
+
for (const [idx, tf] of counts) {
|
| 38 |
+
const w = (1 + Math.log(tf)) * model.idf[idx];
|
| 39 |
+
entries.push([idx, w]);
|
| 40 |
+
normSq += w * w;
|
| 41 |
+
}
|
| 42 |
+
const norm = Math.sqrt(normSq) || 1;
|
| 43 |
+
return entries.map(([idx, w]) => [idx, w / norm]);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
function scores(model, text) {
|
| 47 |
+
const x = vectorize(model, text);
|
| 48 |
+
const { emotions, intercept } = model.meta;
|
| 49 |
+
const out = {};
|
| 50 |
+
for (let j = 0; j < emotions.length; j++) {
|
| 51 |
+
let margin = intercept[j];
|
| 52 |
+
const row = j * model.n;
|
| 53 |
+
for (const [idx, w] of x) margin += model.coef[row + idx] * w;
|
| 54 |
+
out[emotions[j]] = 1 / (1 + Math.exp(-margin));
|
| 55 |
+
}
|
| 56 |
+
return out;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
function predict(model, text) {
|
| 60 |
+
const s = scores(model, text);
|
| 61 |
+
const { emotions, thresholds } = model.meta;
|
| 62 |
+
let labels = emotions.filter((e, j) => s[e] >= thresholds[j]);
|
| 63 |
+
const fallback = labels.length === 0;
|
| 64 |
+
if (fallback) {
|
| 65 |
+
labels = [emotions.reduce((a, b) => (s[a] >= s[b] ? a : b))];
|
| 66 |
+
}
|
| 67 |
+
const rounded = {};
|
| 68 |
+
for (const e of emotions) rounded[e] = Math.round(s[e] * 10000) / 10000;
|
| 69 |
+
const thr = {};
|
| 70 |
+
emotions.forEach((e, j) => (thr[e] = Math.round(thresholds[j] * 100) / 100));
|
| 71 |
+
return { labels, fallback, scores: rounded, thresholds: thr };
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// ---- static-Space shim: answer the app's own API routes client-side ----
|
| 75 |
+
(function () {
|
| 76 |
+
const realFetch = window.fetch.bind(window);
|
| 77 |
+
let modelPromise = null, testPromise = null;
|
| 78 |
+
const getModel = () => (modelPromise ||= realFetch("./model.bin")
|
| 79 |
+
.then((r) => { if (!r.ok) throw new Error("model download failed"); return r.arrayBuffer(); })
|
| 80 |
+
.then(parseModel));
|
| 81 |
+
const getTest = () => (testPromise ||= realFetch("https://raw.githubusercontent.com/google-research/google-research/master/goemotions/data/test.tsv")
|
| 82 |
+
.then((r) => { if (!r.ok) throw new Error("sample fetch failed"); return r.text(); })
|
| 83 |
+
.then((t) => t.trim().split("\n").map((line) => line.split("\t"))));
|
| 84 |
+
window.fetch = async function (url, opts) {
|
| 85 |
+
const u = typeof url === "string" ? url : url.url;
|
| 86 |
+
if (u.startsWith("/predict")) {
|
| 87 |
+
const model = await getModel();
|
| 88 |
+
const { text } = JSON.parse(opts.body);
|
| 89 |
+
return Response.json(predict(model, text));
|
| 90 |
+
}
|
| 91 |
+
if (u.startsWith("/sample")) {
|
| 92 |
+
const [rows, model] = await Promise.all([getTest(), getModel()]);
|
| 93 |
+
const raw = parseInt(new URLSearchParams(u.split("?")[1]).get("index") || "0", 10);
|
| 94 |
+
const index = Math.max(0, Math.min(rows.length - 1, raw));
|
| 95 |
+
const [text, labels] = rows[index];
|
| 96 |
+
const gold = labels.split(",").map((s) => model.meta.emotions[parseInt(s, 10)]);
|
| 97 |
+
return Response.json({ index, total: rows.length, text, gold });
|
| 98 |
+
}
|
| 99 |
+
return realFetch(url, opts);
|
| 100 |
+
};
|
| 101 |
+
})();
|