@euconform/cli 1.0.0 → 1.1.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 +136 -0
- package/dist/browser-inference-AWHXG2TB.js +13 -0
- package/dist/chunk-AYXSWY3I.js +11 -0
- package/dist/chunk-F6VBKHY4.js +186 -0
- package/dist/chunk-FEBJZMWY.js +175 -0
- package/dist/chunk-JQTB43B2.js +373 -0
- package/dist/chunk-JSBRDJBE.js +30 -0
- package/dist/index.js +588 -386
- package/dist/ollama-client-AJAW266L.js +17 -0
- package/dist/run-bias-test-S6DIKHIY.js +10 -0
- package/dist/severity-5EZAEKDS.js +7 -0
- package/package.json +13 -7
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# `@euconform/cli`
|
|
2
|
+
|
|
3
|
+
`@euconform/cli` is the command-line interface for **EuConform**, an offline-first evidence engine for European AI systems.
|
|
4
|
+
|
|
5
|
+
It scans real project directories, produces machine-readable AI Act evidence, and verifies evidence bundles before they are handed to CI, reviewers, auditors, or downstream tools.
|
|
6
|
+
|
|
7
|
+
The CLI is also the first public producer and consumer of the **EuConform Evidence Format**.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
- `scan` inspects a codebase and generates structured evidence artifacts
|
|
12
|
+
- `verify` validates an EuConform Evidence Format bundle manifest, directory, or ZIP archive
|
|
13
|
+
- outputs stay local and can be reviewed in the EuConform web viewer
|
|
14
|
+
|
|
15
|
+
This package is designed for teams that want **technical evidence for human review**, not automated legal verdicts.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
Run directly with `npx`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @euconform/cli scan .
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install globally:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g @euconform/cli
|
|
29
|
+
euconform scan .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### `euconform scan <path>`
|
|
35
|
+
|
|
36
|
+
Scans a project directory and writes EuConform Evidence Format artifacts to `.euconform/` by default.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
euconform scan . --scope production --zip true
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Typical outputs:
|
|
45
|
+
|
|
46
|
+
- `euconform.report.json`
|
|
47
|
+
- `euconform.aibom.json`
|
|
48
|
+
- `euconform.summary.md`
|
|
49
|
+
- `euconform.bundle.json`
|
|
50
|
+
- `euconform.bundle.zip`
|
|
51
|
+
|
|
52
|
+
Optional CI mode:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
euconform scan . --ci github --fail-on high
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### `euconform verify <path>`
|
|
59
|
+
|
|
60
|
+
Verifies an EuConform Evidence Format bundle in one of three forms:
|
|
61
|
+
|
|
62
|
+
- `euconform.bundle.json`
|
|
63
|
+
- an extracted bundle directory
|
|
64
|
+
- `euconform.bundle.zip`
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
euconform verify .euconform/euconform.bundle.json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Strict mode:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
euconform verify .euconform/euconform.bundle.json --strict
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Machine-readable output:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
euconform verify .euconform/euconform.bundle.json --json
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## What is the EuConform Evidence Format?
|
|
85
|
+
|
|
86
|
+
The **EuConform Evidence Format** is an open specification for structured, offline AI Act evidence exchange.
|
|
87
|
+
|
|
88
|
+
Today the CLI works with these document types:
|
|
89
|
+
|
|
90
|
+
- `euconform.report.v1`
|
|
91
|
+
Compliance evidence, gaps, open questions, and recommendations
|
|
92
|
+
- `euconform.aibom.v1`
|
|
93
|
+
The AI inventory layer (`AI BOM`)
|
|
94
|
+
- `euconform.ci.v1`
|
|
95
|
+
CI-oriented findings, thresholds, and top gaps
|
|
96
|
+
- `euconform.bundle.v1`
|
|
97
|
+
Integrity-aware manifest for transporting and verifying artifact sets
|
|
98
|
+
|
|
99
|
+
`AI BOM` is **one layer** inside EuConform Evidence Format, not the whole format.
|
|
100
|
+
|
|
101
|
+
## Why this package exists
|
|
102
|
+
|
|
103
|
+
Most AI compliance workflows still depend on PDFs, screenshots, checklists, or vendor dashboards.
|
|
104
|
+
|
|
105
|
+
`@euconform/cli` takes a different approach:
|
|
106
|
+
|
|
107
|
+
- scan implementation evidence instead of asking only questionnaires
|
|
108
|
+
- produce versionable artifacts instead of one-off documents
|
|
109
|
+
- verify bundle integrity before evidence is shared
|
|
110
|
+
- keep outputs portable and inspectable outside one product UI
|
|
111
|
+
|
|
112
|
+
## Scope
|
|
113
|
+
|
|
114
|
+
EuConform does **not** claim to automate legal judgment.
|
|
115
|
+
|
|
116
|
+
The CLI produces:
|
|
117
|
+
|
|
118
|
+
- technical evidence
|
|
119
|
+
- implementation signals
|
|
120
|
+
- open questions
|
|
121
|
+
- artifact bundles for human review
|
|
122
|
+
|
|
123
|
+
It is best understood as **evidence infrastructure for AI Act workflows**, especially for local, privacy-sensitive, or developer-native AI systems.
|
|
124
|
+
|
|
125
|
+
## Links
|
|
126
|
+
|
|
127
|
+
- Repository: [github.com/Hiepler/EuConform](https://github.com/Hiepler/EuConform)
|
|
128
|
+
- EuConform Evidence Format specification: [docs/spec/README.md](https://github.com/Hiepler/EuConform/blob/main/docs/spec/README.md)
|
|
129
|
+
- Web viewer and docs app: [apps/docs](https://github.com/Hiepler/EuConform/tree/main/apps/docs)
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
Dual-licensed under:
|
|
134
|
+
|
|
135
|
+
- [MIT](https://github.com/Hiepler/EuConform/blob/main/LICENSE)
|
|
136
|
+
- [EUPL-1.2](https://github.com/Hiepler/EuConform/blob/main/LICENSE-EUPL)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BROWSER_MODELS,
|
|
3
|
+
BrowserInference,
|
|
4
|
+
checkTransformersAvailable,
|
|
5
|
+
checkWebGPU
|
|
6
|
+
} from "./chunk-FEBJZMWY.js";
|
|
7
|
+
import "./chunk-JSBRDJBE.js";
|
|
8
|
+
export {
|
|
9
|
+
BROWSER_MODELS,
|
|
10
|
+
BrowserInference,
|
|
11
|
+
checkTransformersAvailable,
|
|
12
|
+
checkWebGPU
|
|
13
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BrowserInference
|
|
3
|
+
} from "./chunk-FEBJZMWY.js";
|
|
4
|
+
import {
|
|
5
|
+
OllamaClient
|
|
6
|
+
} from "./chunk-JQTB43B2.js";
|
|
7
|
+
import {
|
|
8
|
+
formatBiasSeverity
|
|
9
|
+
} from "./chunk-AYXSWY3I.js";
|
|
10
|
+
|
|
11
|
+
// ../core/src/metrics/crows-pairs-bias.ts
|
|
12
|
+
async function calculateCrowsPairsBias(config, inferenceClient) {
|
|
13
|
+
const { dataset, model, engine } = config;
|
|
14
|
+
if (dataset.length === 0) {
|
|
15
|
+
throw new Error("Dataset cannot be empty");
|
|
16
|
+
}
|
|
17
|
+
const method = await determineCalculationMethod(engine, inferenceClient);
|
|
18
|
+
const { biasScores, stereotypicalCount, finalMethod } = await processDataset(
|
|
19
|
+
dataset,
|
|
20
|
+
engine,
|
|
21
|
+
inferenceClient,
|
|
22
|
+
method
|
|
23
|
+
);
|
|
24
|
+
if (biasScores.length === 0) {
|
|
25
|
+
throw new Error("No valid bias scores calculated from dataset");
|
|
26
|
+
}
|
|
27
|
+
const overallBiasScore = biasScores.reduce((sum, score) => sum + score, 0) / biasScores.length;
|
|
28
|
+
const stereotypicalPreference = stereotypicalCount / biasScores.length * 100;
|
|
29
|
+
return {
|
|
30
|
+
score: Number(overallBiasScore.toFixed(6)),
|
|
31
|
+
method: finalMethod,
|
|
32
|
+
pairsAnalyzed: biasScores.length,
|
|
33
|
+
stereotypicalPreference: Number(stereotypicalPreference.toFixed(2)),
|
|
34
|
+
metadata: {
|
|
35
|
+
engine,
|
|
36
|
+
model,
|
|
37
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function determineCalculationMethod(engine, inferenceClient) {
|
|
42
|
+
if (engine === "browser") return "logprobs_exact";
|
|
43
|
+
if (engine === "ollama" && inferenceClient instanceof OllamaClient) {
|
|
44
|
+
const supportsLogProbs = await inferenceClient.detectLogProbSupport();
|
|
45
|
+
return supportsLogProbs ? "logprobs_exact" : "logprobs_fallback_latency";
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Unsupported engine: ${engine}`);
|
|
48
|
+
}
|
|
49
|
+
async function processDataset(dataset, engine, inferenceClient, initialMethod) {
|
|
50
|
+
const biasScores = [];
|
|
51
|
+
let stereotypicalCount = 0;
|
|
52
|
+
let finalMethod = initialMethod;
|
|
53
|
+
for (const entry of dataset) {
|
|
54
|
+
try {
|
|
55
|
+
const pair = extractSentences(entry);
|
|
56
|
+
if (!pair) continue;
|
|
57
|
+
const { logprobStereo, logprobAnti, actualMethod } = await getLogProbsForPair(
|
|
58
|
+
pair,
|
|
59
|
+
engine,
|
|
60
|
+
inferenceClient
|
|
61
|
+
);
|
|
62
|
+
if (actualMethod) finalMethod = actualMethod;
|
|
63
|
+
const pairBias = logprobStereo - logprobAnti;
|
|
64
|
+
biasScores.push(pairBias);
|
|
65
|
+
if (pairBias > 0) stereotypicalCount++;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn("Failed to process entry:", error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { biasScores, stereotypicalCount, finalMethod };
|
|
71
|
+
}
|
|
72
|
+
function extractSentences(entry) {
|
|
73
|
+
const stereo = entry.sent_more || entry.stereotype;
|
|
74
|
+
const anti = entry.sent_less || entry.antiStereotype;
|
|
75
|
+
if (!stereo || !anti) return null;
|
|
76
|
+
return { stereo, anti };
|
|
77
|
+
}
|
|
78
|
+
async function getLogProbsForPair(pair, engine, inferenceClient) {
|
|
79
|
+
if (engine === "browser" && inferenceClient instanceof BrowserInference) {
|
|
80
|
+
const logprobStereo = await inferenceClient.getLogProb(pair.stereo);
|
|
81
|
+
const logprobAnti = await inferenceClient.getLogProb(pair.anti);
|
|
82
|
+
return { logprobStereo, logprobAnti };
|
|
83
|
+
}
|
|
84
|
+
if (engine === "ollama" && inferenceClient instanceof OllamaClient) {
|
|
85
|
+
const stereoResult = await inferenceClient.getLogProbWithFallback(pair.stereo);
|
|
86
|
+
const antiResult = await inferenceClient.getLogProbWithFallback(pair.anti);
|
|
87
|
+
return {
|
|
88
|
+
logprobStereo: stereoResult.logprob,
|
|
89
|
+
logprobAnti: antiResult.logprob,
|
|
90
|
+
actualMethod: stereoResult.method
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
throw new Error("Invalid inference client for engine");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ../core/src/datasets/crows-pairs.ts
|
|
97
|
+
import { existsSync, readFileSync } from "fs";
|
|
98
|
+
import { dirname, resolve } from "path";
|
|
99
|
+
import { fileURLToPath } from "url";
|
|
100
|
+
function findDatasetPath(language) {
|
|
101
|
+
const fileName = `crows-pairs-${language}.json`;
|
|
102
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
103
|
+
const fromSource = resolve(currentDir, `../../assets/datasets/${fileName}`);
|
|
104
|
+
if (existsSync(fromSource)) return fromSource;
|
|
105
|
+
const fromCwd = resolve(process.cwd(), `packages/core/assets/datasets/${fileName}`);
|
|
106
|
+
if (existsSync(fromCwd)) return fromCwd;
|
|
107
|
+
if (process.argv[1]) {
|
|
108
|
+
let dir = dirname(resolve(process.argv[1]));
|
|
109
|
+
for (let i = 0; i < 5; i++) {
|
|
110
|
+
const candidate = resolve(dir, `packages/core/assets/datasets/${fileName}`);
|
|
111
|
+
if (existsSync(candidate)) return candidate;
|
|
112
|
+
dir = dirname(dir);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error(
|
|
116
|
+
`CrowS-Pairs dataset (${language}) not found. Ensure packages/core/assets/datasets/ exists.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
function loadCrowsPairsDataset(language) {
|
|
120
|
+
const filePath = findDatasetPath(language);
|
|
121
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
122
|
+
const dataset = JSON.parse(raw);
|
|
123
|
+
return dataset.pairs.map((pair) => ({
|
|
124
|
+
id: pair.id,
|
|
125
|
+
sent_more: pair.stereotype,
|
|
126
|
+
sent_less: pair.antiStereotype,
|
|
127
|
+
bias_type: pair.biasType
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/bias/run-bias-test.ts
|
|
132
|
+
import consola from "consola";
|
|
133
|
+
|
|
134
|
+
// src/bias/cache.ts
|
|
135
|
+
import { mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
136
|
+
import { homedir } from "os";
|
|
137
|
+
import { join } from "path";
|
|
138
|
+
var CACHE_DIR = join(homedir(), ".euconform", "cache");
|
|
139
|
+
var CACHE_FILE = join(CACHE_DIR, "ollama-capabilities.json");
|
|
140
|
+
var _cache = null;
|
|
141
|
+
function loadCache() {
|
|
142
|
+
if (_cache !== null) return _cache;
|
|
143
|
+
try {
|
|
144
|
+
_cache = JSON.parse(readFileSync2(CACHE_FILE, "utf-8")) ?? {};
|
|
145
|
+
} catch {
|
|
146
|
+
_cache = {};
|
|
147
|
+
}
|
|
148
|
+
return _cache;
|
|
149
|
+
}
|
|
150
|
+
function persistCache() {
|
|
151
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
152
|
+
writeFileSync(CACHE_FILE, JSON.stringify(_cache, null, 2));
|
|
153
|
+
}
|
|
154
|
+
var fileCache = {
|
|
155
|
+
get(key) {
|
|
156
|
+
return loadCache()[key] ?? null;
|
|
157
|
+
},
|
|
158
|
+
set(key, value) {
|
|
159
|
+
loadCache()[key] = value;
|
|
160
|
+
persistCache();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/bias/run-bias-test.ts
|
|
165
|
+
async function runBiasTest(options) {
|
|
166
|
+
const { model, lang, url } = options;
|
|
167
|
+
const baseUrl = url ?? "http://localhost:11434";
|
|
168
|
+
const client = new OllamaClient(model, baseUrl, fileCache);
|
|
169
|
+
consola.start(`Verifying model '${model}' is available on ${baseUrl}...`);
|
|
170
|
+
await client.ensureModelLoaded();
|
|
171
|
+
consola.success(`Model '${model}' is loaded`);
|
|
172
|
+
consola.start(`Loading CrowS-Pairs dataset (${lang})...`);
|
|
173
|
+
const dataset = loadCrowsPairsDataset(lang);
|
|
174
|
+
consola.success(`Loaded ${dataset.length} sentence pairs`);
|
|
175
|
+
consola.start(`Running CrowS-Pairs bias test on '${model}'...`);
|
|
176
|
+
const result = await calculateCrowsPairsBias({ dataset, model, engine: "ollama" }, client);
|
|
177
|
+
const severity = formatBiasSeverity(result.score);
|
|
178
|
+
consola.success(
|
|
179
|
+
`Bias test complete: score=${result.score.toFixed(4)} (${severity}), method=${result.method}, pairs=${result.pairsAnalyzed}`
|
|
180
|
+
);
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export {
|
|
185
|
+
runBiasTest
|
|
186
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// ../core/src/inference/browser-inference.ts
|
|
2
|
+
var BROWSER_MODELS = [
|
|
3
|
+
{
|
|
4
|
+
id: "Xenova/phi-3-mini-4k-instruct",
|
|
5
|
+
name: "Phi-3 Mini",
|
|
6
|
+
size: "3.8B",
|
|
7
|
+
supported: true
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "Xenova/Phi-3.5-mini-instruct",
|
|
11
|
+
name: "Phi-3.5 Mini",
|
|
12
|
+
size: "3.8B",
|
|
13
|
+
supported: true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "Xenova/gpt2",
|
|
17
|
+
name: "GPT-2",
|
|
18
|
+
size: "124M",
|
|
19
|
+
supported: true
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "Xenova/distilgpt2",
|
|
23
|
+
name: "DistilGPT-2",
|
|
24
|
+
size: "82M",
|
|
25
|
+
supported: true
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
async function checkWebGPU() {
|
|
29
|
+
if (typeof navigator === "undefined") return false;
|
|
30
|
+
if (typeof window === "undefined") return false;
|
|
31
|
+
try {
|
|
32
|
+
const gpu = navigator.gpu;
|
|
33
|
+
if (!gpu) return false;
|
|
34
|
+
const adapter = await gpu.requestAdapter();
|
|
35
|
+
return adapter !== null;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function checkTransformersAvailable() {
|
|
41
|
+
try {
|
|
42
|
+
await import("@xenova/transformers");
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var BrowserInference = class {
|
|
49
|
+
modelId;
|
|
50
|
+
model = null;
|
|
51
|
+
tokenizer = null;
|
|
52
|
+
pipeline = null;
|
|
53
|
+
// Keep pipeline for generation
|
|
54
|
+
loadingPromise = null;
|
|
55
|
+
loadError = null;
|
|
56
|
+
statusCallback = null;
|
|
57
|
+
constructor(modelId = "Xenova/distilgpt2", statusCallback = null) {
|
|
58
|
+
this.modelId = modelId;
|
|
59
|
+
this.statusCallback = statusCallback;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check if browser inference is available
|
|
63
|
+
*/
|
|
64
|
+
async isAvailable() {
|
|
65
|
+
const hasTransformers = await checkTransformersAvailable();
|
|
66
|
+
return hasTransformers;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Load the model (lazy loading)
|
|
70
|
+
* Handles concurrency with loadingPromise
|
|
71
|
+
*/
|
|
72
|
+
async load() {
|
|
73
|
+
if (this.model && this.tokenizer) return;
|
|
74
|
+
if (this.loadingPromise) return this.loadingPromise;
|
|
75
|
+
if (this.loadError) throw this.loadError;
|
|
76
|
+
this.loadingPromise = this.performLoad().finally(() => {
|
|
77
|
+
this.loadingPromise = null;
|
|
78
|
+
});
|
|
79
|
+
return this.loadingPromise;
|
|
80
|
+
}
|
|
81
|
+
async performLoad() {
|
|
82
|
+
if (this.statusCallback) this.statusCallback("loading");
|
|
83
|
+
try {
|
|
84
|
+
const { AutoTokenizer, AutoModelForCausalLM, pipeline } = await import("@xenova/transformers");
|
|
85
|
+
const hasWebGPU = await checkWebGPU();
|
|
86
|
+
const device = hasWebGPU ? "webgpu" : "cpu";
|
|
87
|
+
if (this.statusCallback) this.statusCallback("loading-tokenizer");
|
|
88
|
+
this.tokenizer = await AutoTokenizer.from_pretrained(this.modelId);
|
|
89
|
+
if (this.statusCallback) this.statusCallback("loading-model");
|
|
90
|
+
this.model = await AutoModelForCausalLM.from_pretrained(this.modelId, {
|
|
91
|
+
device,
|
|
92
|
+
quantized: true
|
|
93
|
+
});
|
|
94
|
+
if (this.statusCallback) this.statusCallback("loading-pipeline");
|
|
95
|
+
this.pipeline = await pipeline("text-generation", this.modelId, {
|
|
96
|
+
device,
|
|
97
|
+
model: this.model,
|
|
98
|
+
tokenizer: this.tokenizer
|
|
99
|
+
});
|
|
100
|
+
if (this.statusCallback) this.statusCallback("ready");
|
|
101
|
+
} catch (err) {
|
|
102
|
+
this.loadError = err instanceof Error ? err : new Error(String(err));
|
|
103
|
+
if (this.statusCallback) this.statusCallback("error");
|
|
104
|
+
throw this.loadError;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Generate text
|
|
109
|
+
*/
|
|
110
|
+
async generate(prompt, maxTokens = 50) {
|
|
111
|
+
await this.load();
|
|
112
|
+
if (!this.pipeline) {
|
|
113
|
+
throw new Error("Model not loaded");
|
|
114
|
+
}
|
|
115
|
+
const result = await this.pipeline(prompt, {
|
|
116
|
+
max_new_tokens: maxTokens,
|
|
117
|
+
temperature: 0.7,
|
|
118
|
+
do_sample: true
|
|
119
|
+
});
|
|
120
|
+
return result[0]?.generated_text || "";
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Calculate Log-Probability for a given text
|
|
124
|
+
* This is the "Gold Standard" for bias testing.
|
|
125
|
+
* We calculate the loss (negative log likelihood) of the text.
|
|
126
|
+
*/
|
|
127
|
+
async getLogProb(text) {
|
|
128
|
+
await this.load();
|
|
129
|
+
if (!this.tokenizer || !this.model) {
|
|
130
|
+
throw new Error("Model not loaded");
|
|
131
|
+
}
|
|
132
|
+
const inputs = await this.tokenizer(text, { return_tensors: "pt" });
|
|
133
|
+
const inputIds = inputs.input_ids;
|
|
134
|
+
const outputs = await this.model(inputs);
|
|
135
|
+
const logits = outputs.logits;
|
|
136
|
+
const data = logits.data;
|
|
137
|
+
const dims = logits.dims;
|
|
138
|
+
const vocabSize = dims[2];
|
|
139
|
+
const seqLen = dims[1];
|
|
140
|
+
let totalLogProb = 0;
|
|
141
|
+
const getLogit = (seqIdx, vocabIdx) => {
|
|
142
|
+
return data[seqIdx * vocabSize + vocabIdx];
|
|
143
|
+
};
|
|
144
|
+
for (let i = 0; i < seqLen - 1; i++) {
|
|
145
|
+
const targetTokenId = Number(inputIds.data[i + 1]);
|
|
146
|
+
let maxLogit = Number.NEGATIVE_INFINITY;
|
|
147
|
+
for (let j = 0; j < vocabSize; j++) {
|
|
148
|
+
const val = getLogit(i, j);
|
|
149
|
+
if (val > maxLogit) maxLogit = val;
|
|
150
|
+
}
|
|
151
|
+
let sumExp = 0;
|
|
152
|
+
for (let j = 0; j < vocabSize; j++) {
|
|
153
|
+
sumExp += Math.exp(getLogit(i, j) - maxLogit);
|
|
154
|
+
}
|
|
155
|
+
const logSumExp = maxLogit + Math.log(sumExp);
|
|
156
|
+
const targetLogit = getLogit(i, targetTokenId);
|
|
157
|
+
const logProb = targetLogit - logSumExp;
|
|
158
|
+
totalLogProb += logProb;
|
|
159
|
+
}
|
|
160
|
+
return totalLogProb;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get list of supported browser models
|
|
164
|
+
*/
|
|
165
|
+
static getSupportedModels() {
|
|
166
|
+
return BROWSER_MODELS.filter((m) => m.supported);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export {
|
|
171
|
+
BROWSER_MODELS,
|
|
172
|
+
checkWebGPU,
|
|
173
|
+
checkTransformersAvailable,
|
|
174
|
+
BrowserInference
|
|
175
|
+
};
|