moodring / shim.js
usmar's picture
Upload folder using huggingface_hub
8a62f41 verified
Raw
History Blame Contribute Delete
4.09 kB
// JS runtime for the exported moodring model. Mirrors the sklearn pipeline:
// tokenize (runs of >=2 word chars, lowercased), unigrams+bigrams, sublinear
// tf, idf, l2 norm, then per-label logistic scores. Works in browser and Node.
const TOKEN_RE = /[\p{L}\p{N}_]{2,}/gu;
function parseModel(buffer) {
const bytes = new Uint8Array(buffer);
const magic = new TextDecoder().decode(bytes.subarray(0, 9));
if (magic !== "MOODRING1") throw new Error("bad model file");
const jsonLen = new DataView(buffer).getUint32(9, true);
const meta = JSON.parse(new TextDecoder().decode(bytes.subarray(13, 13 + jsonLen)));
const n = meta.n_features;
let off = 13 + jsonLen;
const idf = new Float32Array(buffer.slice(off, off + 4 * n));
off += 4 * n;
const coef = new Float32Array(buffer.slice(off, off + 4 * n * meta.emotions.length));
const vocab = new Map(meta.terms.map((t, i) => [t, i]));
return { meta, idf, coef, vocab, n };
}
function tokenize(text) {
return (text.toLowerCase().match(TOKEN_RE) || []);
}
function vectorize(model, text) {
const tokens = tokenize(text);
const grams = tokens.slice();
for (let i = 0; i + 1 < tokens.length; i++) grams.push(tokens[i] + " " + tokens[i + 1]);
const counts = new Map();
for (const g of grams) {
const idx = model.vocab.get(g);
if (idx !== undefined) counts.set(idx, (counts.get(idx) || 0) + 1);
}
let normSq = 0;
const entries = [];
for (const [idx, tf] of counts) {
const w = (1 + Math.log(tf)) * model.idf[idx];
entries.push([idx, w]);
normSq += w * w;
}
const norm = Math.sqrt(normSq) || 1;
return entries.map(([idx, w]) => [idx, w / norm]);
}
function scores(model, text) {
const x = vectorize(model, text);
const { emotions, intercept } = model.meta;
const out = {};
for (let j = 0; j < emotions.length; j++) {
let margin = intercept[j];
const row = j * model.n;
for (const [idx, w] of x) margin += model.coef[row + idx] * w;
out[emotions[j]] = 1 / (1 + Math.exp(-margin));
}
return out;
}
function predict(model, text) {
const s = scores(model, text);
const { emotions, thresholds } = model.meta;
let labels = emotions.filter((e, j) => s[e] >= thresholds[j]);
const fallback = labels.length === 0;
if (fallback) {
labels = [emotions.reduce((a, b) => (s[a] >= s[b] ? a : b))];
}
const rounded = {};
for (const e of emotions) rounded[e] = Math.round(s[e] * 10000) / 10000;
const thr = {};
emotions.forEach((e, j) => (thr[e] = Math.round(thresholds[j] * 100) / 100));
return { labels, fallback, scores: rounded, thresholds: thr };
}
// ---- static-Space shim: answer the app's own API routes client-side ----
(function () {
const realFetch = window.fetch.bind(window);
let modelPromise = null, testPromise = null;
const getModel = () => (modelPromise ||= realFetch("./model.bin")
.then((r) => { if (!r.ok) throw new Error("model download failed"); return r.arrayBuffer(); })
.then(parseModel));
const getTest = () => (testPromise ||= realFetch("https://raw.githubusercontent.com/google-research/google-research/master/goemotions/data/test.tsv")
.then((r) => { if (!r.ok) throw new Error("sample fetch failed"); return r.text(); })
.then((t) => t.trim().split("\n").map((line) => line.split("\t"))));
window.fetch = async function (url, opts) {
const u = typeof url === "string" ? url : url.url;
if (u.startsWith("/predict")) {
const model = await getModel();
const { text } = JSON.parse(opts.body);
return Response.json(predict(model, text));
}
if (u.startsWith("/sample")) {
const [rows, model] = await Promise.all([getTest(), getModel()]);
const raw = parseInt(new URLSearchParams(u.split("?")[1]).get("index") || "0", 10);
const index = Math.max(0, Math.min(rows.length - 1, raw));
const [text, labels] = rows[index];
const gold = labels.split(",").map((s) => model.meta.emotions[parseInt(s, 10)]);
return Response.json({ index, total: rows.length, text, gold });
}
return realFetch(url, opts);
};
})();