jev-web 0.3.2 → 0.3.3
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/LICENSE +21 -0
- package/README.md +201 -201
- package/package.json +55 -47
- package/src/answers.mjs +36 -36
- package/src/config.mjs +65 -65
- package/src/encode.mjs +83 -83
- package/src/index.mjs +42 -42
- package/src/laya.mjs +625 -625
- package/src/questions.mjs +39 -39
- package/src/registry.mjs +102 -102
- package/src/session.mjs +151 -151
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 warsang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,201 +1,201 @@
|
|
|
1
|
-
# jev-web
|
|
2
|
-
|
|
3
|
-
Run **open-jev-shaped typed-decision models** in the browser. One *state* text
|
|
4
|
-
plus any number of typed *questions* (`choice` / `score` / `noul`) go in; a
|
|
5
|
-
calibrated probability distribution per question comes back from a single
|
|
6
|
-
forward pass. No server, no API keys — weights and inference stay on-device.
|
|
7
|
-
|
|
8
|
-
This is a generic runtime, not a classifier for any particular domain. You
|
|
9
|
-
bring the state text and the questions; the model answers by choosing among
|
|
10
|
-
the options you give it.
|
|
11
|
-
|
|
12
|
-
## Install
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npm i jev-web @huggingface/transformers
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
`@huggingface/transformers` is an optional peer dependency (it is imported
|
|
19
|
-
lazily). Hosts that already ship their own copy can inject it via
|
|
20
|
-
`createDecider({ transformers })`.
|
|
21
|
-
|
|
22
|
-
## Use
|
|
23
|
-
|
|
24
|
-
```js
|
|
25
|
-
import { createDecider } from "jev-web";
|
|
26
|
-
|
|
27
|
-
const decider = await createDecider({
|
|
28
|
-
// defaults to the reference open-jev DeBERTa-v3-large ONNX export,
|
|
29
|
-
// pinned by revision; any repo with the same typed-decision graph works
|
|
30
|
-
onProgress: (p) => console.log(p.phase, p.file, p.progress),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const { answers } = await decider.decide(
|
|
34
|
-
"I was charged twice for the same order and nobody answers my emails.",
|
|
35
|
-
[
|
|
36
|
-
{
|
|
37
|
-
type: "choice",
|
|
38
|
-
instructions: "Which product area is the message about?",
|
|
39
|
-
options: ["fees & charges", "refund & dispute", "card", "other"],
|
|
40
|
-
},
|
|
41
|
-
{ type: "noul", instructions: "The customer is asking for a refund." },
|
|
42
|
-
{
|
|
43
|
-
type: "score",
|
|
44
|
-
instructions: "How negative is the message?",
|
|
45
|
-
options: ["very negative", "negative", "neutral", "positive", "very positive"],
|
|
46
|
-
},
|
|
47
|
-
],
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// [
|
|
51
|
-
// { type: "choice", choice: "refund & dispute", index: 1, probabilities: {...}, confidence: 0.61 },
|
|
52
|
-
// { type: "noul", noul: 0.93, probabilities: { no: 0.07, yes: 0.93 }, confidence: 0.93 },
|
|
53
|
-
// { type: "score", score: 0.4, level: 1, probabilities: {...}, confidence: 0.55 },
|
|
54
|
-
// ]
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Question types
|
|
58
|
-
|
|
59
|
-
| type | options | answer |
|
|
60
|
-
|---|---|---|
|
|
61
|
-
| `choice` | 2–255 labels | `choice` (argmax), `probabilities`, `confidence` |
|
|
62
|
-
| `score` | 2–10 ordered levels | `score` (expected level index, may fall between levels) |
|
|
63
|
-
| `noul` | exactly 2 (defaults `["no","yes"]`) | `noul` (p of the second option = p(yes)), `confidence` |
|
|
64
|
-
|
|
65
|
-
Every answer carries the full distribution. `decide()` also returns
|
|
66
|
-
`{ truncated, length }` — the state is capped (256 tokens by default) and the
|
|
67
|
-
flag tells you when it was cut.
|
|
68
|
-
|
|
69
|
-
## Backends and quantization
|
|
70
|
-
|
|
71
|
-
`device` / `dtype` default to `auto`: WebGPU → `q4f16`, otherwise WASM → `q4`.
|
|
72
|
-
Override explicitly (`fp16`, `fp32`, `q8`, …) when accuracy matters more than
|
|
73
|
-
download size. First load downloads the weights once; transformers.js caches
|
|
74
|
-
them (Cache API / IndexedDB) so later visits are offline-fast.
|
|
75
|
-
|
|
76
|
-
`decider.info` reports the resolved `{ model, revision, device, dtype,
|
|
77
|
-
temperature, maxStateTokens, maxLen, pool, configSource }`.
|
|
78
|
-
|
|
79
|
-
## Model config
|
|
80
|
-
|
|
81
|
-
Defaults come from the model repo's `open_jev_config.json` when present
|
|
82
|
-
(temperature, marker tokens, state cap), else the published reference values.
|
|
83
|
-
To use a different typed-decision export:
|
|
84
|
-
|
|
85
|
-
```js
|
|
86
|
-
createDecider({ model: "your-org/your-typed-decision-ONNX", revision: "main" });
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
The export must add the `[STATE]`, `[Q]`, `[OPT]` markers to its tokenizer,
|
|
90
|
-
take `input_ids, attention_mask, seg, pair_q, pair_opt`, and return per-pair
|
|
91
|
-
`logits`. `typedMarkerIds()` fails loudly when a repo is missing them.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
## Multiple families (registry)
|
|
95
|
-
|
|
96
|
-
`jev-web` is a multi-family runtime: `createDecisionRuntime({ family, fallback })`
|
|
97
|
-
tries the requested family and then any fallbacks, returning the shared contract
|
|
98
|
-
`{ info, decide(state, questions), dispose? }`:
|
|
99
|
-
|
|
100
|
-
```js
|
|
101
|
-
import { createDecisionRuntime, listDecisionFamilies } from "jev-web";
|
|
102
|
-
|
|
103
|
-
listDecisionFamilies(); // ["open-jev", "laya"]
|
|
104
|
-
const runtime = await createDecisionRuntime({
|
|
105
|
-
family: "laya",
|
|
106
|
-
fallback: ["open-jev"],
|
|
107
|
-
onFallback: ({ family, error }) => console.warn(family, error),
|
|
108
|
-
});
|
|
109
|
-
const { answers, truncated, length, prompts, timings } = await runtime.decide(state, questions);
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
Third-party families plug in without touching core:
|
|
113
|
-
|
|
114
|
-
```js
|
|
115
|
-
registerDecisionFamily("my-model", {
|
|
116
|
-
defaults: { model: "org/my-typed-decision-ONNX", revision: "main" },
|
|
117
|
-
create: async (opts) => ({ info, decide, dispose }),
|
|
118
|
-
});
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
`decide()` returns `prompts` (decoded input sequence(s), for audit/debug) and
|
|
122
|
-
`timings` (per-stage ms) alongside the answers.
|
|
123
|
-
|
|
124
|
-
## Laya family (ModernBERT encoder + typed head)
|
|
125
|
-
|
|
126
|
-
`createLayaDecider(options)` runs the [Laya](https://huggingface.co/convaiinnovations/laya)
|
|
127
|
-
System-1 decision models, whose browser export is a **two-file q8 ONNX split**
|
|
128
|
-
(encoder + head) rather than one fused graph:
|
|
129
|
-
|
|
130
|
-
```js
|
|
131
|
-
import { createLayaDecider } from "jev-web";
|
|
132
|
-
|
|
133
|
-
const decider = await createLayaDecider({
|
|
134
|
-
// defaults to alfred361/laya-typed-decisions-web-q8 @ pinned revision
|
|
135
|
-
wasmPaths: "/assets/", // self-hosted ORT wasm (optional)
|
|
136
|
-
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
137
|
-
});
|
|
138
|
-
const { answers } = await decider.decide(stateText, questions);
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
Protocol implemented here (mirrors the reference `laya` Python runtime):
|
|
142
|
-
|
|
143
|
-
- sequence `[CLS] <type> question: <instructions> [SEP] [MASK]opt0 … [SEP] <state> [SEP]`,
|
|
144
|
-
`marker_pos` at each option's `[MASK]`, `qtype` 0 choice / 1 score / 2 noul;
|
|
145
|
-
- encoder `input_ids, attention_mask → hidden`; head
|
|
146
|
-
`hidden, attention_mask, marker_pos, marker_mask, qtype → logits`;
|
|
147
|
-
- per-question calibration from `rl_agent_config.json`
|
|
148
|
-
(`temperature` + `temperature_by_options` buckets, clamped to [0.5, 5]).
|
|
149
|
-
|
|
150
|
-
Notes:
|
|
151
|
-
- the q8 web head is **batch-1 only**, so `decide()` batches the encoder and
|
|
152
|
-
runs the head once per question;
|
|
153
|
-
- `choice` supports at most `max_prefixes` options (6 for the typed-decisions
|
|
154
|
-
export);
|
|
155
|
-
- weights download once (~520 MB q8) and are browser-cached.
|
|
156
|
-
|
|
157
|
-
Lower-level helpers are exported for custom runtimes:
|
|
158
|
-
`buildLayaSequence`, `collateLayaItems`, `layaAnswersFromLogits`,
|
|
159
|
-
`layaTokenizerAdapter`, `renderLayaOptions`, `layaTempBucket`, `clampTemperature`.
|
|
160
|
-
|
|
161
|
-
## Worker / thread
|
|
162
|
-
|
|
163
|
-
`jev-web` does not bundle a worker, so hosts can create one the way their
|
|
164
|
-
bundler prefers:
|
|
165
|
-
|
|
166
|
-
```js
|
|
167
|
-
// host code
|
|
168
|
-
const worker = new Worker(new URL("./my-worker.mjs", import.meta.url), { type: "module" });
|
|
169
|
-
// my-worker.mjs
|
|
170
|
-
import { createDecider } from "jev-web";
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## API
|
|
174
|
-
|
|
175
|
-
- `createDecider(options)` → open-jev family: `{ info, decide, decideMany, dispose }`
|
|
176
|
-
- `createLayaDecider(options)` → Laya family: `{ info, decide, dispose }` (same `decide` contract)
|
|
177
|
-
- `normalizeQuestions`, `buildDecisionInput`, `typedMarkerIds`, `answersFromScores`,
|
|
178
|
-
`softmaxWithTemperature`, `resolveDevice`, `resolveDtype`, `detectWebGPU`,
|
|
179
|
-
`loadModelConfig`, defaults (model/revision/temperature/caps).
|
|
180
|
-
|
|
181
|
-
## Playground
|
|
182
|
-
|
|
183
|
-
```bash
|
|
184
|
-
npm run dev # packages/jev-web → http://localhost:5188
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
Generic presets plus a paste-your-own state/questions box, download progress,
|
|
188
|
-
latency, and a model-cache reset.
|
|
189
|
-
|
|
190
|
-
## Limits
|
|
191
|
-
|
|
192
|
-
- Typed-decision models answer **only** by picking among your options; they do
|
|
193
|
-
not explain or generate text.
|
|
194
|
-
- Accuracy is domain-bound: questions and states unlike the model's training
|
|
195
|
-
domain are out of distribution. Measure before relying on any answer.
|
|
196
|
-
- English, 512-token sequence (state capped), one state per forward pass.
|
|
197
|
-
|
|
198
|
-
## License
|
|
199
|
-
|
|
200
|
-
MIT (runtime code). Model weights keep their own license (the reference export
|
|
201
|
-
is Apache-2.0).
|
|
1
|
+
# jev-web
|
|
2
|
+
|
|
3
|
+
Run **open-jev-shaped typed-decision models** in the browser. One *state* text
|
|
4
|
+
plus any number of typed *questions* (`choice` / `score` / `noul`) go in; a
|
|
5
|
+
calibrated probability distribution per question comes back from a single
|
|
6
|
+
forward pass. No server, no API keys — weights and inference stay on-device.
|
|
7
|
+
|
|
8
|
+
This is a generic runtime, not a classifier for any particular domain. You
|
|
9
|
+
bring the state text and the questions; the model answers by choosing among
|
|
10
|
+
the options you give it.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i jev-web @huggingface/transformers
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@huggingface/transformers` is an optional peer dependency (it is imported
|
|
19
|
+
lazily). Hosts that already ship their own copy can inject it via
|
|
20
|
+
`createDecider({ transformers })`.
|
|
21
|
+
|
|
22
|
+
## Use
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { createDecider } from "jev-web";
|
|
26
|
+
|
|
27
|
+
const decider = await createDecider({
|
|
28
|
+
// defaults to the reference open-jev DeBERTa-v3-large ONNX export,
|
|
29
|
+
// pinned by revision; any repo with the same typed-decision graph works
|
|
30
|
+
onProgress: (p) => console.log(p.phase, p.file, p.progress),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { answers } = await decider.decide(
|
|
34
|
+
"I was charged twice for the same order and nobody answers my emails.",
|
|
35
|
+
[
|
|
36
|
+
{
|
|
37
|
+
type: "choice",
|
|
38
|
+
instructions: "Which product area is the message about?",
|
|
39
|
+
options: ["fees & charges", "refund & dispute", "card", "other"],
|
|
40
|
+
},
|
|
41
|
+
{ type: "noul", instructions: "The customer is asking for a refund." },
|
|
42
|
+
{
|
|
43
|
+
type: "score",
|
|
44
|
+
instructions: "How negative is the message?",
|
|
45
|
+
options: ["very negative", "negative", "neutral", "positive", "very positive"],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// [
|
|
51
|
+
// { type: "choice", choice: "refund & dispute", index: 1, probabilities: {...}, confidence: 0.61 },
|
|
52
|
+
// { type: "noul", noul: 0.93, probabilities: { no: 0.07, yes: 0.93 }, confidence: 0.93 },
|
|
53
|
+
// { type: "score", score: 0.4, level: 1, probabilities: {...}, confidence: 0.55 },
|
|
54
|
+
// ]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Question types
|
|
58
|
+
|
|
59
|
+
| type | options | answer |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `choice` | 2–255 labels | `choice` (argmax), `probabilities`, `confidence` |
|
|
62
|
+
| `score` | 2–10 ordered levels | `score` (expected level index, may fall between levels) |
|
|
63
|
+
| `noul` | exactly 2 (defaults `["no","yes"]`) | `noul` (p of the second option = p(yes)), `confidence` |
|
|
64
|
+
|
|
65
|
+
Every answer carries the full distribution. `decide()` also returns
|
|
66
|
+
`{ truncated, length }` — the state is capped (256 tokens by default) and the
|
|
67
|
+
flag tells you when it was cut.
|
|
68
|
+
|
|
69
|
+
## Backends and quantization
|
|
70
|
+
|
|
71
|
+
`device` / `dtype` default to `auto`: WebGPU → `q4f16`, otherwise WASM → `q4`.
|
|
72
|
+
Override explicitly (`fp16`, `fp32`, `q8`, …) when accuracy matters more than
|
|
73
|
+
download size. First load downloads the weights once; transformers.js caches
|
|
74
|
+
them (Cache API / IndexedDB) so later visits are offline-fast.
|
|
75
|
+
|
|
76
|
+
`decider.info` reports the resolved `{ model, revision, device, dtype,
|
|
77
|
+
temperature, maxStateTokens, maxLen, pool, configSource }`.
|
|
78
|
+
|
|
79
|
+
## Model config
|
|
80
|
+
|
|
81
|
+
Defaults come from the model repo's `open_jev_config.json` when present
|
|
82
|
+
(temperature, marker tokens, state cap), else the published reference values.
|
|
83
|
+
To use a different typed-decision export:
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
createDecider({ model: "your-org/your-typed-decision-ONNX", revision: "main" });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The export must add the `[STATE]`, `[Q]`, `[OPT]` markers to its tokenizer,
|
|
90
|
+
take `input_ids, attention_mask, seg, pair_q, pair_opt`, and return per-pair
|
|
91
|
+
`logits`. `typedMarkerIds()` fails loudly when a repo is missing them.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## Multiple families (registry)
|
|
95
|
+
|
|
96
|
+
`jev-web` is a multi-family runtime: `createDecisionRuntime({ family, fallback })`
|
|
97
|
+
tries the requested family and then any fallbacks, returning the shared contract
|
|
98
|
+
`{ info, decide(state, questions), dispose? }`:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { createDecisionRuntime, listDecisionFamilies } from "jev-web";
|
|
102
|
+
|
|
103
|
+
listDecisionFamilies(); // ["open-jev", "laya"]
|
|
104
|
+
const runtime = await createDecisionRuntime({
|
|
105
|
+
family: "laya",
|
|
106
|
+
fallback: ["open-jev"],
|
|
107
|
+
onFallback: ({ family, error }) => console.warn(family, error),
|
|
108
|
+
});
|
|
109
|
+
const { answers, truncated, length, prompts, timings } = await runtime.decide(state, questions);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Third-party families plug in without touching core:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
registerDecisionFamily("my-model", {
|
|
116
|
+
defaults: { model: "org/my-typed-decision-ONNX", revision: "main" },
|
|
117
|
+
create: async (opts) => ({ info, decide, dispose }),
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`decide()` returns `prompts` (decoded input sequence(s), for audit/debug) and
|
|
122
|
+
`timings` (per-stage ms) alongside the answers.
|
|
123
|
+
|
|
124
|
+
## Laya family (ModernBERT encoder + typed head)
|
|
125
|
+
|
|
126
|
+
`createLayaDecider(options)` runs the [Laya](https://huggingface.co/convaiinnovations/laya)
|
|
127
|
+
System-1 decision models, whose browser export is a **two-file q8 ONNX split**
|
|
128
|
+
(encoder + head) rather than one fused graph:
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
import { createLayaDecider } from "jev-web";
|
|
132
|
+
|
|
133
|
+
const decider = await createLayaDecider({
|
|
134
|
+
// defaults to alfred361/laya-typed-decisions-web-q8 @ pinned revision
|
|
135
|
+
wasmPaths: "/assets/", // self-hosted ORT wasm (optional)
|
|
136
|
+
onProgress: (p) => console.log(p.phase, p.loaded, p.total),
|
|
137
|
+
});
|
|
138
|
+
const { answers } = await decider.decide(stateText, questions);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Protocol implemented here (mirrors the reference `laya` Python runtime):
|
|
142
|
+
|
|
143
|
+
- sequence `[CLS] <type> question: <instructions> [SEP] [MASK]opt0 … [SEP] <state> [SEP]`,
|
|
144
|
+
`marker_pos` at each option's `[MASK]`, `qtype` 0 choice / 1 score / 2 noul;
|
|
145
|
+
- encoder `input_ids, attention_mask → hidden`; head
|
|
146
|
+
`hidden, attention_mask, marker_pos, marker_mask, qtype → logits`;
|
|
147
|
+
- per-question calibration from `rl_agent_config.json`
|
|
148
|
+
(`temperature` + `temperature_by_options` buckets, clamped to [0.5, 5]).
|
|
149
|
+
|
|
150
|
+
Notes:
|
|
151
|
+
- the q8 web head is **batch-1 only**, so `decide()` batches the encoder and
|
|
152
|
+
runs the head once per question;
|
|
153
|
+
- `choice` supports at most `max_prefixes` options (6 for the typed-decisions
|
|
154
|
+
export);
|
|
155
|
+
- weights download once (~520 MB q8) and are browser-cached.
|
|
156
|
+
|
|
157
|
+
Lower-level helpers are exported for custom runtimes:
|
|
158
|
+
`buildLayaSequence`, `collateLayaItems`, `layaAnswersFromLogits`,
|
|
159
|
+
`layaTokenizerAdapter`, `renderLayaOptions`, `layaTempBucket`, `clampTemperature`.
|
|
160
|
+
|
|
161
|
+
## Worker / thread
|
|
162
|
+
|
|
163
|
+
`jev-web` does not bundle a worker, so hosts can create one the way their
|
|
164
|
+
bundler prefers:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// host code
|
|
168
|
+
const worker = new Worker(new URL("./my-worker.mjs", import.meta.url), { type: "module" });
|
|
169
|
+
// my-worker.mjs
|
|
170
|
+
import { createDecider } from "jev-web";
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## API
|
|
174
|
+
|
|
175
|
+
- `createDecider(options)` → open-jev family: `{ info, decide, decideMany, dispose }`
|
|
176
|
+
- `createLayaDecider(options)` → Laya family: `{ info, decide, dispose }` (same `decide` contract)
|
|
177
|
+
- `normalizeQuestions`, `buildDecisionInput`, `typedMarkerIds`, `answersFromScores`,
|
|
178
|
+
`softmaxWithTemperature`, `resolveDevice`, `resolveDtype`, `detectWebGPU`,
|
|
179
|
+
`loadModelConfig`, defaults (model/revision/temperature/caps).
|
|
180
|
+
|
|
181
|
+
## Playground
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
npm run dev # packages/jev-web → http://localhost:5188
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Generic presets plus a paste-your-own state/questions box, download progress,
|
|
188
|
+
latency, and a model-cache reset.
|
|
189
|
+
|
|
190
|
+
## Limits
|
|
191
|
+
|
|
192
|
+
- Typed-decision models answer **only** by picking among your options; they do
|
|
193
|
+
not explain or generate text.
|
|
194
|
+
- Accuracy is domain-bound: questions and states unlike the model's training
|
|
195
|
+
domain are out of distribution. Measure before relying on any answer.
|
|
196
|
+
- English, 512-token sequence (state capped), one state per forward pass.
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT (runtime code). Model weights keep their own license (the reference export
|
|
201
|
+
is Apache-2.0).
|
package/package.json
CHANGED
|
@@ -1,47 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "jev-web",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Run open-jev- and Laya-shaped typed-decision models in the browser: one state plus typed questions in, calibrated probabilities out.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
"
|
|
41
|
-
"@huggingface/transformers":
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "jev-web",
|
|
3
|
+
"version": "0.3.3",
|
|
4
|
+
"description": "Run open-jev- and Laya-shaped typed-decision models in the browser: one state plus typed questions in, calibrated probabilities out.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/warsang/jev-web.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/warsang/jev-web#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/warsang/jev-web/issues"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.mjs"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src/",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"open-jev",
|
|
24
|
+
"typed-decisions",
|
|
25
|
+
"onnx",
|
|
26
|
+
"transformers.js",
|
|
27
|
+
"webgpu",
|
|
28
|
+
"browser",
|
|
29
|
+
"classification"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"dev": "vite",
|
|
33
|
+
"build:dev": "vite build",
|
|
34
|
+
"test": "node --test test/*.test.mjs"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@huggingface/transformers": ">=3.0.0",
|
|
38
|
+
"onnxruntime-web": ">=1.17.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"@huggingface/transformers": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"onnxruntime-web": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@huggingface/transformers": "^4.3.0",
|
|
50
|
+
"vite": "^6.3.5"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=20"
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/answers.mjs
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
// jev-web — score → answer decoding for typed-decision models.
|
|
2
|
-
//
|
|
3
|
-
// The graph returns one logit per (question, option) pair. Each question's
|
|
4
|
-
// options form a softmax group; a post-hoc temperature (fitted by the model
|
|
5
|
-
// authors) calibrates the distribution.
|
|
6
|
-
|
|
7
|
-
export function softmaxWithTemperature(logits, temperature = 1) {
|
|
8
|
-
const t = Number.isFinite(temperature) && temperature > 0 ? temperature : 1;
|
|
9
|
-
if (!Array.isArray(logits) || logits.length === 0) throw new TypeError("logits must be a non-empty array");
|
|
10
|
-
const max = Math.max(...logits);
|
|
11
|
-
const exps = logits.map((x) => Math.exp((Number(x) - max) / t));
|
|
12
|
-
const sum = exps.reduce((a, b) => a + b, 0);
|
|
13
|
-
return exps.map((e) => e / sum);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function answersFromScores(scores, { questions, groups, temperature = 1 }) {
|
|
17
|
-
if (!Array.isArray(scores)) throw new TypeError("scores must be an array");
|
|
18
|
-
return questions.map((q, i) => {
|
|
19
|
-
const pairIndices = groups[i];
|
|
20
|
-
const probs = softmaxWithTemperature(pairIndices.map((p) => scores[p]), temperature);
|
|
21
|
-
const probabilities = Object.fromEntries(q.options.map((option, j) => [option, probs[j]]));
|
|
22
|
-
const confidence = Math.max(...probs);
|
|
23
|
-
const best = probs.indexOf(confidence);
|
|
24
|
-
|
|
25
|
-
if (q.type === "choice") {
|
|
26
|
-
return { type: "choice", choice: q.options[best], index: best, probabilities, confidence };
|
|
27
|
-
}
|
|
28
|
-
if (q.type === "score") {
|
|
29
|
-
const expected = probs.reduce((acc, p, j) => acc + p * j, 0);
|
|
30
|
-
return { type: "score", score: expected, level: best, probabilities, confidence };
|
|
31
|
-
}
|
|
32
|
-
// noul: probabilities are { no, yes }; the answer is p(yes).
|
|
33
|
-
const yes = probabilities[q.options[1]];
|
|
34
|
-
return { type: "noul", noul: yes, probabilities, confidence: Math.max(yes, 1 - yes) };
|
|
35
|
-
});
|
|
36
|
-
}
|
|
1
|
+
// jev-web — score → answer decoding for typed-decision models.
|
|
2
|
+
//
|
|
3
|
+
// The graph returns one logit per (question, option) pair. Each question's
|
|
4
|
+
// options form a softmax group; a post-hoc temperature (fitted by the model
|
|
5
|
+
// authors) calibrates the distribution.
|
|
6
|
+
|
|
7
|
+
export function softmaxWithTemperature(logits, temperature = 1) {
|
|
8
|
+
const t = Number.isFinite(temperature) && temperature > 0 ? temperature : 1;
|
|
9
|
+
if (!Array.isArray(logits) || logits.length === 0) throw new TypeError("logits must be a non-empty array");
|
|
10
|
+
const max = Math.max(...logits);
|
|
11
|
+
const exps = logits.map((x) => Math.exp((Number(x) - max) / t));
|
|
12
|
+
const sum = exps.reduce((a, b) => a + b, 0);
|
|
13
|
+
return exps.map((e) => e / sum);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function answersFromScores(scores, { questions, groups, temperature = 1 }) {
|
|
17
|
+
if (!Array.isArray(scores)) throw new TypeError("scores must be an array");
|
|
18
|
+
return questions.map((q, i) => {
|
|
19
|
+
const pairIndices = groups[i];
|
|
20
|
+
const probs = softmaxWithTemperature(pairIndices.map((p) => scores[p]), temperature);
|
|
21
|
+
const probabilities = Object.fromEntries(q.options.map((option, j) => [option, probs[j]]));
|
|
22
|
+
const confidence = Math.max(...probs);
|
|
23
|
+
const best = probs.indexOf(confidence);
|
|
24
|
+
|
|
25
|
+
if (q.type === "choice") {
|
|
26
|
+
return { type: "choice", choice: q.options[best], index: best, probabilities, confidence };
|
|
27
|
+
}
|
|
28
|
+
if (q.type === "score") {
|
|
29
|
+
const expected = probs.reduce((acc, p, j) => acc + p * j, 0);
|
|
30
|
+
return { type: "score", score: expected, level: best, probabilities, confidence };
|
|
31
|
+
}
|
|
32
|
+
// noul: probabilities are { no, yes }; the answer is p(yes).
|
|
33
|
+
const yes = probabilities[q.options[1]];
|
|
34
|
+
return { type: "noul", noul: yes, probabilities, confidence: Math.max(yes, 1 - yes) };
|
|
35
|
+
});
|
|
36
|
+
}
|