@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 +15 -2
- package/dist/hub.d.ts +1 -1
- package/dist/hub.js +1 -1
- package/dist/index.browser.js +14 -1
- package/dist/model.d.ts +4 -0
- package/dist/model.js +160 -14
- package/package.json +11 -2
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
|
|
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.
|
|
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.
|
|
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/index.browser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
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]
|
|
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.
|
|
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
|
-
"
|
|
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",
|