@desert-ant-labs/emo 0.5.0 → 0.6.1

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
@@ -1,8 +1,13 @@
1
1
  # @desert-ant-labs/emo
2
2
 
3
+ **On-device emoji suggestion for Node and the browser. TypeScript-native, offline, multilingual, no inference runtime.**
4
+
3
5
  On-device emoji suggestions from text. Suggests the best-matching emoji for short
4
6
  tasks, calendar entries, notes, or message drafts across **23 languages**, fully
5
- in-process, no inference runtime.
7
+ in-process, no inference runtime. Emo does emoji prediction (text-to-emoji), useful
8
+ for a keyboard, an autocomplete, or any text field. It runs in TypeScript and
9
+ JavaScript, in Node and in the browser. Emo suggests emoji from text; it is not an
10
+ emoji picker.
6
11
 
7
12
  ```ts
8
13
  import { suggestions } from "@desert-ant-labs/emo";
@@ -17,7 +22,7 @@ const toned = (await suggestions("go for a run", 1, { skinTone: "medium" }))[0]?
17
22
  ## Features
18
23
 
19
24
  - Pure-JS inference (no ONNX/WASM runtime); prediction is sub-millisecond
20
- - Suggests from a data-driven vocabulary of ~500 task/calendar/message emojis
25
+ - Suggests from a curated vocabulary of ~800 everyday emojis (task, message, and concrete nouns)
21
26
  - Supports 23 languages (incl. CJK, Arabic, Thai, Hindi, …)
22
27
  - Model (~5.0 MB, 4-bit palettized) is fetched from the Hugging Face Hub at a **pinned revision**, then
23
28
  cached, to the **filesystem** on Node and to **Cache Storage** in the browser, so
@@ -112,6 +117,14 @@ emoji for each task on-device. Run it with `node server.js` from that folder and
112
117
 
113
118
  Published at [`desert-ant-labs/emo`](https://huggingface.co/desert-ant-labs/emo) on Hugging Face.
114
119
 
120
+ ## Other platforms
121
+
122
+ Same model, native on each platform:
123
+
124
+ - [`emo-swift`](https://github.com/Desert-Ant-Labs/emo-swift): Swift for iOS and macOS
125
+ - [`emo-kotlin`](https://github.com/Desert-Ant-Labs/emo-kotlin): Kotlin for Android and the JVM
126
+ - Model weights and card: [`desert-ant-labs/emo`](https://huggingface.co/desert-ant-labs/emo)
127
+
115
128
  ## License
116
129
 
117
130
  [Desert Ant Labs Source-Available License](https://license.desertant.ai/1.0). Free for
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}`;
@@ -1,3 +1,4 @@
1
+ import { initUsage } from "@desert-ant-labs/desert-ant-web";
1
2
  import { webCache } from "./cache-web.js";
2
3
  import { DEFAULT_HOST, DEFAULT_REPO, DEFAULT_REVISION, loadModel } from "./hub.js";
3
4
  export { createEmo, EmoModel } from "./model.js";
@@ -11,10 +12,22 @@ export const env = {
11
12
  allowRemote: true,
12
13
  useCache: true,
13
14
  };
15
+ const USAGE_KEY = "dal_lEL3EuFU2eh8IRTH8RW9pV9czYn0TrCk";
16
+ let usage = null;
17
+ function instrument(model) {
18
+ const suggestions = model.suggestions.bind(model);
19
+ model.suggestions = (text, limit, options) => {
20
+ usage ??= initUsage({ key: USAGE_KEY });
21
+ const out = suggestions(text, limit, options);
22
+ usage.recordCall();
23
+ return out;
24
+ };
25
+ return model;
26
+ }
14
27
  /** Loads the model from the Hugging Face Hub (cached in Cache Storage). */
15
28
  export async function load(options = {}) {
16
29
  const e = { ...env, ...options };
17
- return loadModel(e, e.useCache ? webCache() : null);
30
+ return instrument(await loadModel(e, e.useCache ? webCache() : null));
18
31
  }
19
32
  let modelPromise = null;
20
33
  /**
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.1",
4
4
  "description": "On-device emoji suggestions from text.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.node.js",
@@ -51,9 +51,15 @@
51
51
  },
52
52
  "keywords": [
53
53
  "emoji",
54
+ "emoji-suggestion",
55
+ "emoji-prediction",
56
+ "text-to-emoji",
57
+ "emoji-autocomplete",
54
58
  "on-device",
59
+ "offline",
55
60
  "multilingual",
56
- "text-classification"
61
+ "typescript",
62
+ "keyboard"
57
63
  ],
58
64
  "license": "SEE LICENSE IN LICENSE.md",
59
65
  "repository": {
@@ -68,6 +74,9 @@
68
74
  "publishConfig": {
69
75
  "access": "public"
70
76
  },
77
+ "dependencies": {
78
+ "@desert-ant-labs/desert-ant-web": "^0.1.0"
79
+ },
71
80
  "devDependencies": {
72
81
  "@arethetypeswrong/cli": "^0.18",
73
82
  "@types/node": "^22",