@desert-ant-labs/emo 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ const toned = (await suggestions("go for a run", 1, { skinTone: "medium" }))[0]?
17
17
  ## Features
18
18
 
19
19
  - Pure-JS inference (no ONNX/WASM runtime); prediction is sub-millisecond
20
- - Suggests from a data-driven vocabulary of ~500 task/calendar/message emojis
20
+ - Suggests from a curated vocabulary of ~800 everyday emojis (task, message, and concrete nouns)
21
21
  - Supports 23 languages (incl. CJK, Arabic, Thai, Hindi, …)
22
22
  - Model (~5.0 MB, 4-bit palettized) is fetched from the Hugging Face Hub at a **pinned revision**, then
23
23
  cached, to the **filesystem** on Node and to **Cache Storage** in the browser, so
package/dist/hub.d.ts CHANGED
@@ -3,7 +3,7 @@ export declare const DEFAULT_HOST = "https://huggingface.co";
3
3
  export declare const DEFAULT_REPO = "desert-ant-labs/emo";
4
4
  /** Pinned revision of the model repo. A tag (not a bare commit SHA) so it
5
5
  * survives history rewrites/squashes on the model repo. */
6
- export declare const DEFAULT_REVISION = "v0.5.0";
6
+ export declare const DEFAULT_REVISION = "v0.6.0";
7
7
  /** Resolution + caching configuration (mutate the exported `env` to change defaults). */
8
8
  export interface EmoEnv {
9
9
  /** Hugging Face host serving the model repo. */
package/dist/hub.js CHANGED
@@ -3,7 +3,7 @@ export const DEFAULT_HOST = "https://huggingface.co";
3
3
  export const DEFAULT_REPO = "desert-ant-labs/emo";
4
4
  /** Pinned revision of the model repo. A tag (not a bare commit SHA) so it
5
5
  * survives history rewrites/squashes on the model repo. */
6
- export const DEFAULT_REVISION = "v0.5.0";
6
+ export const DEFAULT_REVISION = "v0.6.0";
7
7
  const FILES = ["emo.safetensors", "emo_tokenizer.bin", "emo_meta.json"];
8
8
  function resolveUrl(env, name) {
9
9
  return `${env.host}/${env.repo}/resolve/${env.revision}/${name}`;
package/dist/model.d.ts CHANGED
@@ -14,6 +14,10 @@ export interface EmoMeta {
14
14
  n_importance: number;
15
15
  sem_dim: number;
16
16
  sem_pad_index: number;
17
+ /** "transformer" runs the semantic sequence through an encoder; absent = mean-pool. */
18
+ arch?: string;
19
+ n_layers?: number;
20
+ n_heads?: number;
17
21
  }
18
22
  /** Loads the bundled model and runs emoji inference. Construct once and reuse. */
19
23
  export declare class EmoModel {
package/dist/model.js CHANGED
@@ -31,6 +31,88 @@ function erf(x) {
31
31
  return x >= 0 ? y : -y;
32
32
  }
33
33
  const gelu = (x) => 0.5 * x * (1 + erf(x / Math.SQRT2));
34
+ // ---- transformer helpers (semantic sequence path) -------------------------
35
+ // S is a row-major T x inDim buffer; returns T x outDim = S @ W^T + b (W is outDim x inDim).
36
+ function linear(S, T, inDim, W, outDim, b) {
37
+ const out = new Float32Array(T * outDim);
38
+ for (let t = 0; t < T; t++) {
39
+ const sb = t * inDim, ob = t * outDim;
40
+ for (let o = 0; o < outDim; o++) {
41
+ let acc = b ? b.get(o) : 0;
42
+ const wb = o * inDim;
43
+ for (let i = 0; i < inDim; i++)
44
+ acc += W.get(wb + i) * S[sb + i];
45
+ out[ob + o] = acc;
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ // in-place post-norm LayerNorm over the last dim.
51
+ function layerNorm(S, T, d, w, b) {
52
+ for (let t = 0; t < T; t++) {
53
+ const base = t * d;
54
+ let mean = 0;
55
+ for (let c = 0; c < d; c++)
56
+ mean += S[base + c];
57
+ mean /= d;
58
+ let v = 0;
59
+ for (let c = 0; c < d; c++) {
60
+ const z = S[base + c] - mean;
61
+ v += z * z;
62
+ }
63
+ const inv = 1 / Math.sqrt(v / d + 1e-5);
64
+ for (let c = 0; c < d; c++)
65
+ S[base + c] = (S[base + c] - mean) * inv * w.get(c) + b.get(c);
66
+ }
67
+ }
68
+ // Post-norm encoder layer (gelu FFN), matching nn.TransformerEncoderLayer.
69
+ function encoderLayer(S, T, d, H, L) {
70
+ const dh = d / H, scale = 1 / Math.sqrt(dh);
71
+ const qkv = linear(S, T, d, L.qkv_w, 3 * d, L.qkv_b); // T x 3d
72
+ const attn = new Float32Array(T * d);
73
+ const scores = new Float32Array(T);
74
+ for (let h = 0; h < H; h++) {
75
+ const off = h * dh;
76
+ for (let ti = 0; ti < T; ti++) {
77
+ const qb = ti * 3 * d + off;
78
+ let mx = -Infinity;
79
+ for (let tj = 0; tj < T; tj++) {
80
+ const kb = tj * 3 * d + d + off;
81
+ let s = 0;
82
+ for (let c = 0; c < dh; c++)
83
+ s += qkv[qb + c] * qkv[kb + c];
84
+ s *= scale;
85
+ scores[tj] = s;
86
+ if (s > mx)
87
+ mx = s;
88
+ }
89
+ let sum = 0;
90
+ for (let tj = 0; tj < T; tj++) {
91
+ const e = Math.exp(scores[tj] - mx);
92
+ scores[tj] = e;
93
+ sum += e;
94
+ }
95
+ const ob = ti * d + off;
96
+ for (let tj = 0; tj < T; tj++) {
97
+ const a = scores[tj] / sum, vb = tj * 3 * d + 2 * d + off;
98
+ for (let c = 0; c < dh; c++)
99
+ attn[ob + c] += a * qkv[vb + c];
100
+ }
101
+ }
102
+ }
103
+ const o = linear(attn, T, d, L.o_w, d, L.o_b); // T x d
104
+ for (let i = 0; i < T * d; i++)
105
+ o[i] += S[i];
106
+ layerNorm(o, T, d, L.n1_w, L.n1_b);
107
+ const ff1 = linear(o, T, d, L.f1_w, L.f1_b.rows, L.f1_b); // T x ff
108
+ for (let i = 0; i < ff1.length; i++)
109
+ ff1[i] = gelu(ff1[i]);
110
+ const ff2 = linear(ff1, T, L.f1_b.rows, L.f2_w, d, L.f2_b); // T x d
111
+ for (let i = 0; i < T * d; i++)
112
+ ff2[i] += o[i];
113
+ layerNorm(ff2, T, d, L.n2_w, L.n2_b);
114
+ return ff2;
115
+ }
34
116
  // Minimal safetensors reader: u64 header length, JSON header, then raw tensor bytes.
35
117
  // Each weight is either raw F32, or a 4-bit k-means palette stored as a packed U8 index
36
118
  // tensor plus a "<name>.palette" F32 tensor (logical 2-D shape kept in __metadata__).
@@ -59,10 +141,27 @@ function parseWeights(bytes) {
59
141
  const [rows, cols = 1] = slice(name).shape;
60
142
  return new Tensor(rows, cols, f32(name), null, null);
61
143
  };
62
- return {
144
+ const base = {
63
145
  embed: expand("embed"), sem: expand("sem"), importance: expand("importance"),
64
146
  w1: expand("w1"), b1: expand("b1"), w2: expand("w2"), b2: expand("b2"),
65
147
  };
148
+ if (!header["ap_w"])
149
+ return { ...base, tr: null };
150
+ // transformer encoder + attention pool
151
+ const nLayers = Object.keys(header).filter((k) => /^L\d+_qkv_w$/.test(k)).length;
152
+ const layers = [];
153
+ for (let l = 0; l < nLayers; l++) {
154
+ layers.push({
155
+ qkv_w: expand(`L${l}_qkv_w`), qkv_b: expand(`L${l}_qkv_b`),
156
+ o_w: expand(`L${l}_o_w`), o_b: expand(`L${l}_o_b`),
157
+ f1_w: expand(`L${l}_f1_w`), f1_b: expand(`L${l}_f1_b`),
158
+ f2_w: expand(`L${l}_f2_w`), f2_b: expand(`L${l}_f2_b`),
159
+ n1_w: expand(`L${l}_n1_w`), n1_b: expand(`L${l}_n1_b`),
160
+ n2_w: expand(`L${l}_n2_w`), n2_b: expand(`L${l}_n2_b`),
161
+ });
162
+ }
163
+ const tr = { layers, ap_w: expand("ap_w"), ap_b: expand("ap_b"), ap_q: expand("ap_q"), pn_w: expand("pn_w"), pn_b: expand("pn_b") };
164
+ return { ...base, tr };
66
165
  }
67
166
  /** Loads the bundled model and runs emoji inference. Construct once and reuse. */
68
167
  export class EmoModel {
@@ -103,21 +202,68 @@ export class EmoModel {
103
202
  if (ids.length === 0)
104
203
  ids = [this.meta.sem_pad_index];
105
204
  const sv = new Float32Array(semDim);
106
- for (const id of ids) {
107
- if (id >= sem.rows)
108
- continue;
109
- const base = id * semDim;
205
+ const tr = this.w.tr;
206
+ if (tr) {
207
+ // semantic sequence -> transformer encoder -> attention pool -> LayerNorm
208
+ const T = ids.length;
209
+ let S = new Float32Array(T * semDim);
210
+ for (let t = 0; t < T; t++) {
211
+ const id = ids[t] < sem.rows ? ids[t] : this.meta.sem_pad_index;
212
+ const sb = id * semDim, ob = t * semDim;
213
+ for (let c = 0; c < semDim; c++)
214
+ S[ob + c] = id < sem.rows ? sem.get(sb + c) : 0;
215
+ }
216
+ const H = this.meta.n_heads ?? 4;
217
+ for (const L of tr.layers)
218
+ S = encoderLayer(S, T, semDim, H, L);
219
+ // attention pool: score_t = (aproj(tanh(S_t)) . q) / sqrt(d), softmax over t, weighted sum
220
+ const scoreScale = 1 / Math.sqrt(semDim);
221
+ const tanhS = new Float32Array(T * semDim);
222
+ for (let i = 0; i < T * semDim; i++)
223
+ tanhS[i] = Math.tanh(S[i]);
224
+ const proj = linear(tanhS, T, semDim, tr.ap_w, semDim, tr.ap_b);
225
+ const sc = new Float32Array(T);
226
+ let mx = -Infinity;
227
+ for (let t = 0; t < T; t++) {
228
+ let s = 0;
229
+ const pb = t * semDim;
230
+ for (let c = 0; c < semDim; c++)
231
+ s += proj[pb + c] * tr.ap_q.get(c);
232
+ s *= scoreScale;
233
+ sc[t] = s;
234
+ if (s > mx)
235
+ mx = s;
236
+ }
237
+ let sum = 0;
238
+ for (let t = 0; t < T; t++) {
239
+ const e = Math.exp(sc[t] - mx);
240
+ sc[t] = e;
241
+ sum += e;
242
+ }
243
+ for (let t = 0; t < T; t++) {
244
+ const a = sc[t] / sum, sb = t * semDim;
245
+ for (let c = 0; c < semDim; c++)
246
+ sv[c] += a * S[sb + c];
247
+ }
248
+ layerNorm(sv, 1, semDim, tr.pn_w, tr.pn_b);
249
+ }
250
+ else {
251
+ for (const id of ids) {
252
+ if (id >= sem.rows)
253
+ continue;
254
+ const base = id * semDim;
255
+ for (let c = 0; c < semDim; c++)
256
+ sv[c] += sem.get(base + c);
257
+ }
258
+ for (let c = 0; c < semDim; c++)
259
+ sv[c] /= ids.length;
260
+ let norm = 0;
261
+ for (let c = 0; c < semDim; c++)
262
+ norm += sv[c] * sv[c];
263
+ norm = Math.sqrt(norm) + 1e-9;
110
264
  for (let c = 0; c < semDim; c++)
111
- sv[c] += sem.get(base + c);
265
+ sv[c] /= norm;
112
266
  }
113
- for (let c = 0; c < semDim; c++)
114
- sv[c] /= ids.length;
115
- let norm = 0;
116
- for (let c = 0; c < semDim; c++)
117
- norm += sv[c] * sv[c];
118
- norm = Math.sqrt(norm) + 1e-9;
119
- for (let c = 0; c < semDim; c++)
120
- sv[c] /= norm;
121
267
  const inDim = ngDim + semDim;
122
268
  const x = new Float32Array(inDim);
123
269
  x.set(ng, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desert-ant-labs/emo",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "On-device emoji suggestions from text.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.node.js",