usmar commited on
Commit
8a62f41
·
verified ·
1 Parent(s): 1670a17

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +10 -5
  2. index.html +197 -18
  3. model.bin +3 -0
  4. shim.js +101 -0
README.md CHANGED
@@ -1,10 +1,15 @@
1
  ---
2
- title: Moodring
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: yellow
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
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
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ })();