@doccloak/core 0.8.0 → 0.10.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/CHANGELOG.md +151 -0
- package/dist/dom/docx.d.ts +29 -2
- package/dist/dom/docx.js +64 -10
- package/dist/dom/index.d.ts +2 -0
- package/dist/dom/index.js +3 -0
- package/dist/dom/office.d.ts +75 -0
- package/dist/dom/office.js +116 -0
- package/dist/dom/opc.d.ts +42 -0
- package/dist/dom/opc.js +146 -0
- package/dist/dom/xlsx.d.ts +64 -0
- package/dist/dom/xlsx.js +280 -0
- package/dist/engine.d.ts +8 -5
- package/dist/engine.js +21 -7
- package/dist/index.d.ts +8 -4
- package/dist/index.js +9 -3
- package/dist/model-loader.d.ts +45 -2
- package/dist/model-loader.js +99 -5
- package/dist/providers/bardsai.d.ts +12 -0
- package/dist/providers/bardsai.js +51 -19
- package/dist/providers/gliner-base.d.ts +92 -0
- package/dist/providers/gliner-base.js +357 -0
- package/dist/providers/gliner.d.ts +30 -2
- package/dist/providers/gliner.js +68 -13
- package/dist/regex/rules.data.js +186 -0
- package/dist/regex/validators.d.ts +7 -0
- package/dist/regex/validators.js +95 -4
- package/dist/restore-tokens.d.ts +67 -0
- package/dist/restore-tokens.js +120 -0
- package/dist/session.d.ts +89 -14
- package/dist/session.js +270 -37
- package/dist/surrogates.d.ts +82 -0
- package/dist/surrogates.js +1510 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +4 -0
- package/package.json +9 -2
- package/rules/gb.json +17 -0
- package/rules/schema.json +2 -0
- package/rules/universal.json +169 -0
package/dist/model-loader.d.ts
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* - Serves the model from the injected BlobCache when available and prefers
|
|
15
15
|
* the cached (typically disk-backed) copy over the in-memory one to reduce
|
|
16
16
|
* peak RAM, which matters on iOS Safari where large tabs get killed.
|
|
17
|
+
* - Verifies downloads against a pinned SHA-256 when the caller provides one
|
|
18
|
+
* (supply-chain hardening, T116): mismatches evict the cache entry and
|
|
19
|
+
* reject with ModelIntegrityError; verified cache entries carry a marker
|
|
20
|
+
* so they are not re-hashed on every startup.
|
|
17
21
|
*
|
|
18
22
|
* Environment-agnostic: all host capabilities (blob cache, fetch, persistent
|
|
19
23
|
* storage request) are injected via ModelLoaderEnv. This module never touches
|
|
@@ -33,9 +37,47 @@ export interface ModelLoaderEnv {
|
|
|
33
37
|
/** Best-effort persistent-storage request (navigator.storage.persist() on the web). */
|
|
34
38
|
persistStorage?: () => Promise<boolean>;
|
|
35
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* A downloaded (or cached) model blob did not match the pinned SHA-256.
|
|
42
|
+
* Distinct error type so hosts can show a dedicated "integrity check
|
|
43
|
+
* failed" state instead of a generic download error. The offending cache
|
|
44
|
+
* entry is evicted before this is thrown; the regex/rules tier keeps
|
|
45
|
+
* working (existing degraded-mode behavior).
|
|
46
|
+
*/
|
|
47
|
+
export declare class ModelIntegrityError extends Error {
|
|
48
|
+
url: string;
|
|
49
|
+
expectedSha256: string;
|
|
50
|
+
actualSha256: string;
|
|
51
|
+
constructor(url: string, expectedSha256: string, actualSha256: string);
|
|
52
|
+
}
|
|
53
|
+
/** Successful SHA-256 verification of a model blob, surfaced to the model-status UI. */
|
|
54
|
+
export interface ModelVerification {
|
|
55
|
+
url: string;
|
|
56
|
+
/** The verified SHA-256, lowercase hex - equals the pinned hash. */
|
|
57
|
+
sha256: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Cache key of the tiny "this URL was verified against this hash" marker.
|
|
61
|
+
* Uses a query parameter (not a URL fragment) because Cache Storage strips
|
|
62
|
+
* fragments, which would collide with the model entry itself. Keyed by
|
|
63
|
+
* url AND hash so bumping a pinned hash automatically invalidates markers
|
|
64
|
+
* written for the previous pin.
|
|
65
|
+
*/
|
|
66
|
+
export declare function verificationMarkerKey(url: string, sha256: string): string;
|
|
36
67
|
export interface FetchModelOptions {
|
|
37
68
|
maxAttempts?: number;
|
|
38
69
|
retryBaseDelayMs?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Pinned SHA-256 (lowercase hex) of the model blob. When set, every
|
|
72
|
+
* network download is hashed before use; a mismatch evicts the cache
|
|
73
|
+
* entry and rejects with ModelIntegrityError. Cached blobs that carry a
|
|
74
|
+
* verification marker for this hash skip re-hashing so startup cost is
|
|
75
|
+
* unchanged; cached blobs without a marker are hashed once and either
|
|
76
|
+
* marked verified or evicted and re-downloaded.
|
|
77
|
+
*/
|
|
78
|
+
sha256?: string;
|
|
79
|
+
/** Fired once the served blob is known to match the pinned sha256. */
|
|
80
|
+
onVerified?: (verification: ModelVerification) => void;
|
|
39
81
|
}
|
|
40
82
|
/**
|
|
41
83
|
* Download a model with progress reporting, resume and retry.
|
|
@@ -43,9 +85,10 @@ export interface FetchModelOptions {
|
|
|
43
85
|
*/
|
|
44
86
|
export declare function fetchModelBlob(env: ModelLoaderEnv, url: string, onProgress?: DownloadProgress, options?: FetchModelOptions): Promise<Blob>;
|
|
45
87
|
/**
|
|
46
|
-
* Remove a single model from the cache (e.g. when its URL changes).
|
|
88
|
+
* Remove a single model from the cache (e.g. when its URL changes). When a
|
|
89
|
+
* pinned sha256 is given, the matching verification marker is removed too.
|
|
47
90
|
*/
|
|
48
|
-
export declare function evictModelFromCache(env: ModelLoaderEnv, url: string): Promise<void>;
|
|
91
|
+
export declare function evictModelFromCache(env: ModelLoaderEnv, url: string, sha256?: string): Promise<void>;
|
|
49
92
|
/**
|
|
50
93
|
* Retry an async operation with exponential backoff. Used for the smaller
|
|
51
94
|
* companion downloads (tokenizer files) that go through libraries without
|
package/dist/model-loader.js
CHANGED
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
* - Serves the model from the injected BlobCache when available and prefers
|
|
15
15
|
* the cached (typically disk-backed) copy over the in-memory one to reduce
|
|
16
16
|
* peak RAM, which matters on iOS Safari where large tabs get killed.
|
|
17
|
+
* - Verifies downloads against a pinned SHA-256 when the caller provides one
|
|
18
|
+
* (supply-chain hardening, T116): mismatches evict the cache entry and
|
|
19
|
+
* reject with ModelIntegrityError; verified cache entries carry a marker
|
|
20
|
+
* so they are not re-hashed on every startup.
|
|
17
21
|
*
|
|
18
22
|
* Environment-agnostic: all host capabilities (blob cache, fetch, persistent
|
|
19
23
|
* storage request) are injected via ModelLoaderEnv. This module never touches
|
|
@@ -30,6 +34,52 @@ class HttpError extends Error {
|
|
|
30
34
|
this.status = status;
|
|
31
35
|
}
|
|
32
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* A downloaded (or cached) model blob did not match the pinned SHA-256.
|
|
39
|
+
* Distinct error type so hosts can show a dedicated "integrity check
|
|
40
|
+
* failed" state instead of a generic download error. The offending cache
|
|
41
|
+
* entry is evicted before this is thrown; the regex/rules tier keeps
|
|
42
|
+
* working (existing degraded-mode behavior).
|
|
43
|
+
*/
|
|
44
|
+
export class ModelIntegrityError extends Error {
|
|
45
|
+
constructor(url, expectedSha256, actualSha256) {
|
|
46
|
+
super(`Model failed SHA-256 integrity check: expected ${expectedSha256.slice(0, 12)}..., ` +
|
|
47
|
+
`got ${actualSha256.slice(0, 12)}... (${url}). The downloaded copy was discarded.`);
|
|
48
|
+
this.name = 'ModelIntegrityError';
|
|
49
|
+
this.url = url;
|
|
50
|
+
this.expectedSha256 = expectedSha256;
|
|
51
|
+
this.actualSha256 = actualSha256;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Cache key of the tiny "this URL was verified against this hash" marker.
|
|
56
|
+
* Uses a query parameter (not a URL fragment) because Cache Storage strips
|
|
57
|
+
* fragments, which would collide with the model entry itself. Keyed by
|
|
58
|
+
* url AND hash so bumping a pinned hash automatically invalidates markers
|
|
59
|
+
* written for the previous pin.
|
|
60
|
+
*/
|
|
61
|
+
export function verificationMarkerKey(url, sha256) {
|
|
62
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
63
|
+
return `${url}${sep}doccloak-sha256-verified=${sha256}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* SHA-256 of a blob as lowercase hex. WebCrypto has no streaming digest,
|
|
67
|
+
* so the blob is hashed as one contiguous buffer: peak cost is one extra
|
|
68
|
+
* transient copy of the model, released before ONNX Runtime allocates its
|
|
69
|
+
* own copy.
|
|
70
|
+
*/
|
|
71
|
+
async function sha256Hex(blob) {
|
|
72
|
+
const buffer = await blob.arrayBuffer();
|
|
73
|
+
const digest = await crypto.subtle.digest('SHA-256', buffer);
|
|
74
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('');
|
|
75
|
+
}
|
|
76
|
+
/** Best-effort write of the verification marker (a 1-byte blob). */
|
|
77
|
+
async function markVerified(cache, url, sha256) {
|
|
78
|
+
try {
|
|
79
|
+
await cache.put(verificationMarkerKey(url, sha256), new Blob(['1']));
|
|
80
|
+
}
|
|
81
|
+
catch { /* marker is an optimisation - next startup just re-hashes */ }
|
|
82
|
+
}
|
|
33
83
|
function isRetryable(err) {
|
|
34
84
|
if (err instanceof HttpError) {
|
|
35
85
|
return err.status === 408 || err.status === 429 || err.status >= 500;
|
|
@@ -80,9 +130,11 @@ async function tryCachePut(cache, url, blob) {
|
|
|
80
130
|
if (!stored) {
|
|
81
131
|
console.warn('[DocCloak] Model cache put refused (model still loaded in memory)');
|
|
82
132
|
}
|
|
133
|
+
return stored;
|
|
83
134
|
}
|
|
84
135
|
catch (err) {
|
|
85
136
|
console.warn('[DocCloak] Model cache put failed (model still loaded in memory):', err);
|
|
137
|
+
return false;
|
|
86
138
|
}
|
|
87
139
|
}
|
|
88
140
|
/**
|
|
@@ -150,11 +202,33 @@ async function downloadAttempt(fetchFn, url, offset, onChunk, onHeaders) {
|
|
|
150
202
|
export async function fetchModelBlob(env, url, onProgress, options) {
|
|
151
203
|
const maxAttempts = options?.maxAttempts ?? MAX_ATTEMPTS;
|
|
152
204
|
const retryBaseDelayMs = options?.retryBaseDelayMs ?? RETRY_BASE_DELAY_MS;
|
|
205
|
+
const expectedSha256 = options?.sha256?.toLowerCase();
|
|
153
206
|
const cache = env.cache;
|
|
154
207
|
const cachedBlob = await readFromCache(cache, url);
|
|
155
208
|
if (cachedBlob) {
|
|
156
|
-
|
|
157
|
-
|
|
209
|
+
if (!expectedSha256) {
|
|
210
|
+
onProgress?.(cachedBlob.size, cachedBlob.size);
|
|
211
|
+
return cachedBlob;
|
|
212
|
+
}
|
|
213
|
+
// Previously verified against this exact pin: skip the re-hash so
|
|
214
|
+
// startup cost is unchanged.
|
|
215
|
+
const marker = await readFromCache(cache, verificationMarkerKey(url, expectedSha256));
|
|
216
|
+
if (marker) {
|
|
217
|
+
onProgress?.(cachedBlob.size, cachedBlob.size);
|
|
218
|
+
options?.onVerified?.({ url, sha256: expectedSha256 });
|
|
219
|
+
return cachedBlob;
|
|
220
|
+
}
|
|
221
|
+
// No marker (cache predates pinning, or the pin changed): hash once.
|
|
222
|
+
const actualSha256 = await sha256Hex(cachedBlob);
|
|
223
|
+
if (actualSha256 === expectedSha256) {
|
|
224
|
+
await markVerified(cache, url, expectedSha256);
|
|
225
|
+
onProgress?.(cachedBlob.size, cachedBlob.size);
|
|
226
|
+
options?.onVerified?.({ url, sha256: expectedSha256 });
|
|
227
|
+
return cachedBlob;
|
|
228
|
+
}
|
|
229
|
+
// Stale or corrupt cached copy - evict and fall through to the network.
|
|
230
|
+
console.warn(`[DocCloak] Cached model failed SHA-256 check (expected ${expectedSha256.slice(0, 12)}..., got ${actualSha256.slice(0, 12)}...), re-downloading`);
|
|
231
|
+
await evictModelFromCache(env, url, expectedSha256);
|
|
158
232
|
}
|
|
159
233
|
await requestPersistentStorage(env);
|
|
160
234
|
let lastProgressAt = 0;
|
|
@@ -213,22 +287,42 @@ export async function fetchModelBlob(env, url, onProgress, options) {
|
|
|
213
287
|
reportProgress(downloaded, total || downloaded, true);
|
|
214
288
|
let blob = new Blob(chunks);
|
|
215
289
|
chunks = [];
|
|
216
|
-
|
|
290
|
+
if (expectedSha256) {
|
|
291
|
+
// Verify the final assembled blob (covers both fresh and resumed
|
|
292
|
+
// downloads) BEFORE it is cached or handed to the runtime.
|
|
293
|
+
const actualSha256 = await sha256Hex(blob);
|
|
294
|
+
if (actualSha256 !== expectedSha256) {
|
|
295
|
+
await evictModelFromCache(env, url, expectedSha256);
|
|
296
|
+
throw new ModelIntegrityError(url, expectedSha256, actualSha256);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const stored = await tryCachePut(cache, url, blob);
|
|
300
|
+
if (expectedSha256 && stored)
|
|
301
|
+
await markVerified(cache, url, expectedSha256);
|
|
217
302
|
// Prefer the cached (disk-backed) copy so the in-memory chunks can be
|
|
218
303
|
// collected before ONNX Runtime allocates its own copy of the model.
|
|
219
304
|
const diskBlob = await readFromCache(cache, url);
|
|
220
305
|
if (diskBlob && diskBlob.size === blob.size)
|
|
221
306
|
blob = diskBlob;
|
|
307
|
+
if (expectedSha256)
|
|
308
|
+
options?.onVerified?.({ url, sha256: expectedSha256 });
|
|
222
309
|
return blob;
|
|
223
310
|
}
|
|
224
311
|
/**
|
|
225
|
-
* Remove a single model from the cache (e.g. when its URL changes).
|
|
312
|
+
* Remove a single model from the cache (e.g. when its URL changes). When a
|
|
313
|
+
* pinned sha256 is given, the matching verification marker is removed too.
|
|
226
314
|
*/
|
|
227
|
-
export async function evictModelFromCache(env, url) {
|
|
315
|
+
export async function evictModelFromCache(env, url, sha256) {
|
|
228
316
|
try {
|
|
229
317
|
await env.cache.delete(url);
|
|
230
318
|
}
|
|
231
319
|
catch { /* ignore */ }
|
|
320
|
+
if (sha256) {
|
|
321
|
+
try {
|
|
322
|
+
await env.cache.delete(verificationMarkerKey(url, sha256.toLowerCase()));
|
|
323
|
+
}
|
|
324
|
+
catch { /* ignore */ }
|
|
325
|
+
}
|
|
232
326
|
}
|
|
233
327
|
/**
|
|
234
328
|
* Retry an async operation with exponential backoff. Used for the smaller
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { DetectedEntity, DetectionProvider, ProgressCallback } from '../types.js';
|
|
14
14
|
import type { CoreEnv } from '../env.js';
|
|
15
|
+
import type { ModelVerification } from '../model-loader.js';
|
|
16
|
+
export declare const BARDSAI_MODEL_REVISION = "0e72e19f030ed4e661b1673e549af8e0dd176386";
|
|
17
|
+
/** SHA-256 of onnx/model_quantized.onnx (278,736,360 bytes) at BARDSAI_MODEL_REVISION. */
|
|
18
|
+
export declare const BARDSAI_MODEL_SHA256 = "8c9f555c743ed14eb7e505ff9d9c7785775a1671fd14031af33f424de8bd0e7b";
|
|
19
|
+
export declare const BARDSAI_MODEL_URL = "https://huggingface.co/bardsai/eu-pii-anonimization-multilang/resolve/0e72e19f030ed4e661b1673e549af8e0dd176386/onnx/model_quantized.onnx";
|
|
15
20
|
export declare class BardsaiProvider implements DetectionProvider {
|
|
16
21
|
private _name;
|
|
17
22
|
get name(): string;
|
|
@@ -23,7 +28,14 @@ export declare class BardsaiProvider implements DetectionProvider {
|
|
|
23
28
|
private loadWaiters;
|
|
24
29
|
private progressCallback;
|
|
25
30
|
private threshold;
|
|
31
|
+
private verification;
|
|
26
32
|
constructor(env: CoreEnv);
|
|
33
|
+
/**
|
|
34
|
+
* SHA-256 verification result of the served model blob (null until the
|
|
35
|
+
* model has been verified against the pinned hash). Surfaced so the
|
|
36
|
+
* model-status UI can show the positive "Model verified" state.
|
|
37
|
+
*/
|
|
38
|
+
getVerification(): ModelVerification | null;
|
|
27
39
|
isLoaded(): boolean;
|
|
28
40
|
isLoading(): boolean;
|
|
29
41
|
onProgress(callback: ProgressCallback): void;
|
|
@@ -10,10 +10,27 @@
|
|
|
10
10
|
* tokenizer all arrive through the injected CoreEnv. This module never touches
|
|
11
11
|
* import.meta, caches, navigator or @huggingface/transformers directly.
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// ort 1.29: the /webgpu entry is the non-deprecated build (native WebGPU EP +
|
|
14
|
+
// wasm fallback, asyncify binary). The bare entry resolves the deprecated JSEP
|
|
15
|
+
// bundle whose wasm exceeds Cloudflare Pages' 25 MiB per-file limit.
|
|
16
|
+
import * as ort from 'onnxruntime-web/webgpu';
|
|
17
|
+
import { evictModelFromCache, fetchModelBlob, retryAsync } from "../model-loader.js";
|
|
15
18
|
// ── Model config ──────────────────────────────────────────
|
|
16
|
-
|
|
19
|
+
// Supply-chain pinning (T116): the model is fetched from an immutable
|
|
20
|
+
// commit revision, never from mutable resolve/main, and the downloaded
|
|
21
|
+
// blob is verified against a pinned SHA-256. A model update is a
|
|
22
|
+
// deliberate act: bump BARDSAI_MODEL_REVISION and BARDSAI_MODEL_SHA256
|
|
23
|
+
// together in a reviewed commit and update
|
|
24
|
+
// documentation/model-provenance.md.
|
|
25
|
+
// Revision: main of bardsai/eu-pii-anonimization-multilang as of its last
|
|
26
|
+
// modification 2026-05-13; pinned and hashed 2026-08-10.
|
|
27
|
+
export const BARDSAI_MODEL_REVISION = '0e72e19f030ed4e661b1673e549af8e0dd176386';
|
|
28
|
+
/** SHA-256 of onnx/model_quantized.onnx (278,736,360 bytes) at BARDSAI_MODEL_REVISION. */
|
|
29
|
+
export const BARDSAI_MODEL_SHA256 = '8c9f555c743ed14eb7e505ff9d9c7785775a1671fd14031af33f424de8bd0e7b';
|
|
30
|
+
export const BARDSAI_MODEL_URL = `https://huggingface.co/bardsai/eu-pii-anonimization-multilang/resolve/${BARDSAI_MODEL_REVISION}/onnx/model_quantized.onnx`;
|
|
31
|
+
const MODEL_URL = BARDSAI_MODEL_URL;
|
|
32
|
+
/** Pre-pinning download URL; its cache entry is evicted best-effort on load. */
|
|
33
|
+
const LEGACY_MODEL_URL = 'https://huggingface.co/bardsai/eu-pii-anonimization-multilang/resolve/main/onnx/model_quantized.onnx';
|
|
17
34
|
const TOKENIZER_HF = 'bardsai/eu-pii-anonimization-multilang';
|
|
18
35
|
const MODEL_NAME = 'BardS.ai EU PII';
|
|
19
36
|
const DEFAULT_THRESHOLD = 0.5;
|
|
@@ -102,8 +119,17 @@ export class BardsaiProvider {
|
|
|
102
119
|
this.loadWaiters = [];
|
|
103
120
|
this.progressCallback = null;
|
|
104
121
|
this.threshold = DEFAULT_THRESHOLD;
|
|
122
|
+
this.verification = null;
|
|
105
123
|
this.env = env;
|
|
106
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* SHA-256 verification result of the served model blob (null until the
|
|
127
|
+
* model has been verified against the pinned hash). Surfaced so the
|
|
128
|
+
* model-status UI can show the positive "Model verified" state.
|
|
129
|
+
*/
|
|
130
|
+
getVerification() {
|
|
131
|
+
return this.verification;
|
|
132
|
+
}
|
|
107
133
|
isLoaded() {
|
|
108
134
|
return this.session !== null && this.tokenizer !== null;
|
|
109
135
|
}
|
|
@@ -141,9 +167,18 @@ export class BardsaiProvider {
|
|
|
141
167
|
fetch: this.env.fetch,
|
|
142
168
|
persistStorage: this.env.persistStorage,
|
|
143
169
|
};
|
|
170
|
+
// Old cache entry from the pre-pinning resolve/main URL - drop it so
|
|
171
|
+
// a 279 MB stale copy does not linger against the origin quota.
|
|
172
|
+
void evictModelFromCache(loaderEnv, LEGACY_MODEL_URL);
|
|
144
173
|
// Load tokenizer and model in parallel (skip tokenizer if already loaded from a prior session)
|
|
145
174
|
const tasks = [
|
|
146
|
-
fetchModelBlob(loaderEnv, MODEL_URL, (downloaded, total) => this.progressCallback?.(downloaded, total)
|
|
175
|
+
fetchModelBlob(loaderEnv, MODEL_URL, (downloaded, total) => this.progressCallback?.(downloaded, total), {
|
|
176
|
+
sha256: BARDSAI_MODEL_SHA256,
|
|
177
|
+
onVerified: (verification) => {
|
|
178
|
+
this.verification = verification;
|
|
179
|
+
console.info(`[DocCloak] Model verified: ${this._name} SHA-256 ${verification.sha256.slice(0, 12)}... matches the published hash (see documentation/model-provenance.md)`);
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
147
182
|
];
|
|
148
183
|
if (!this.tokenizer) {
|
|
149
184
|
tasks.push(retryAsync(() => this.env.loadTokenizer(TOKENIZER_HF), 'Tokenizer download').then((t) => {
|
|
@@ -199,13 +234,11 @@ export class BardsaiProvider {
|
|
|
199
234
|
}
|
|
200
235
|
if (words.length === 0)
|
|
201
236
|
return [];
|
|
202
|
-
// Tokenize full text once to count subtokens per word via ▁ prefix
|
|
237
|
+
// Tokenize full text once to count subtokens per word via ▁ prefix.
|
|
238
|
+
// transformers.js 4.x removed tokenizer.model.convert_ids_to_tokens;
|
|
239
|
+
// tokenize() returns the same SentencePiece token strings directly.
|
|
203
240
|
onProgress?.(0);
|
|
204
|
-
const
|
|
205
|
-
add_special_tokens: false,
|
|
206
|
-
});
|
|
207
|
-
const tokenIds = Array.from(fullEncoded.input_ids.data);
|
|
208
|
-
const tokenStrings = this.tokenizer.model.convert_ids_to_tokens(tokenIds.map(Number));
|
|
241
|
+
const tokenStrings = this.tokenizer.tokenize(words.map((w) => w.word).join(' '));
|
|
209
242
|
// Count subtokens per word: ▁ prefix marks word boundaries (SentencePiece)
|
|
210
243
|
const subtokenCounts = [];
|
|
211
244
|
let currentCount = 0;
|
|
@@ -285,16 +318,15 @@ export class BardsaiProvider {
|
|
|
285
318
|
const attentionMask = encoded.attention_mask.data;
|
|
286
319
|
const seqLen = encoded.input_ids.dims[1];
|
|
287
320
|
// Build word→subtoken alignment via ▁ prefix (no per-word tokenization needed)
|
|
288
|
-
|
|
289
|
-
|
|
321
|
+
// transformers.js 4.x removed tokenizer.model.convert_ids_to_tokens;
|
|
322
|
+
// tokenize() returns the token strings without specials, so positions are
|
|
323
|
+
// offset by 1 for the leading <s> in the encoded sequence and the seqLen
|
|
324
|
+
// guard stops at the truncation boundary.
|
|
325
|
+
const chunkTokenStrings = this.tokenizer.tokenize(chunkText);
|
|
290
326
|
const wordSubtokenStart = [];
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (!tok || tok === '</s>')
|
|
295
|
-
break;
|
|
296
|
-
if (tok.startsWith('\u2581')) {
|
|
297
|
-
wordSubtokenStart.push(t);
|
|
327
|
+
for (let i = 0; i < chunkTokenStrings.length && i + 1 < seqLen - 1; i++) {
|
|
328
|
+
if (chunkTokenStrings[i].startsWith('\u2581')) {
|
|
329
|
+
wordSubtokenStart.push(i + 1);
|
|
298
330
|
}
|
|
299
331
|
}
|
|
300
332
|
// XLM-RoBERTa: only input_ids + attention_mask (no token_type_ids)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLiNER PII Base provider (T122).
|
|
3
|
+
*
|
|
4
|
+
* knowledgator/gliner-pii-base-v1.0: deberta-v3-small backbone, zero-shot
|
|
5
|
+
* custom labels, span_mode "markerV0". Unlike the token_level small/edge
|
|
6
|
+
* models (gliner.ts), the ONNX graph takes span enumeration inputs
|
|
7
|
+
* (span_idx + span_mask) and emits span-level logits shaped
|
|
8
|
+
* [batch, num_words, max_width, num_classes] - one score per candidate
|
|
9
|
+
* (start word, width) pair per label. Verified against the pinned model in
|
|
10
|
+
* Node WASM: inputs [input_ids, attention_mask, words_mask, text_lengths,
|
|
11
|
+
* span_idx (int64), span_mask (bool)], output [logits].
|
|
12
|
+
*
|
|
13
|
+
* Environment-agnostic: wasm paths, custom-label persistence (KV), model
|
|
14
|
+
* bytes (blob cache + fetch) and the tokenizer all arrive through the
|
|
15
|
+
* injected CoreEnv. This module never touches import.meta, localStorage,
|
|
16
|
+
* caches, navigator or @huggingface/transformers directly.
|
|
17
|
+
*/
|
|
18
|
+
import type { DetectedEntity, DetectionProvider, ProgressCallback } from '../types.js';
|
|
19
|
+
import type { CoreEnv } from '../env.js';
|
|
20
|
+
import type { ModelVerification } from '../model-loader.js';
|
|
21
|
+
import { type RawSpan } from './gliner.js';
|
|
22
|
+
export declare const GLINER_BASE_MODEL_REVISION = "61726e0ad791dcab3e29339bbec3ad42ded65641";
|
|
23
|
+
/** SHA-256 of onnx/model_quint8.onnx (196,757,174 bytes) at GLINER_BASE_MODEL_REVISION. */
|
|
24
|
+
export declare const GLINER_BASE_MODEL_SHA256 = "0514c8fd86d0513ce5351a3267f132b57d5bcd8f99a90d43cde1228092881d19";
|
|
25
|
+
export declare const GLINER_BASE_MODEL_URL = "https://huggingface.co/knowledgator/gliner-pii-base-v1.0/resolve/61726e0ad791dcab3e29339bbec3ad42ded65641/onnx/model_quint8.onnx";
|
|
26
|
+
/** max_width from the repo's gliner_config.json: spans cover 1..12 words. */
|
|
27
|
+
export declare const GLINER_BASE_MAX_WIDTH = 12;
|
|
28
|
+
export interface SpanIndices {
|
|
29
|
+
/** Flattened (start, end) word pairs, shape [1, numSpans, 2]. */
|
|
30
|
+
spanIdx: BigInt64Array;
|
|
31
|
+
/** 1 for valid spans (end < textLength), 0 for padding, shape [1, numSpans]. */
|
|
32
|
+
spanMask: Uint8Array;
|
|
33
|
+
numSpans: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Enumerate all candidate word spans exactly like the python GLiNER
|
|
37
|
+
* SpanProcessor: start-major, width-minor - for every start word i, the
|
|
38
|
+
* pairs (i, i+0) .. (i, i+maxWidth-1). Spans that would run past the end
|
|
39
|
+
* of the text are masked out and their indices zeroed (mirroring
|
|
40
|
+
* span_idx * span_mask in the python collate).
|
|
41
|
+
*/
|
|
42
|
+
export declare function buildSpanIndices(textLength: number, maxWidth?: number): SpanIndices;
|
|
43
|
+
/**
|
|
44
|
+
* Decode markerV0 span logits (shape [1, numWords, maxWidth, numClasses])
|
|
45
|
+
* into raw character spans: sigmoid per (start, width, class) cell,
|
|
46
|
+
* threshold, then map word indices to character offsets. Non-overlap
|
|
47
|
+
* selection (greedySelect) happens at the caller across chunks.
|
|
48
|
+
*/
|
|
49
|
+
export declare function decodeSpanLogits(logits: Float32Array, dims: readonly number[], textLength: number, labels: string[], threshold: number, charStarts: number[], charEnds: number[], fullText: string): RawSpan[];
|
|
50
|
+
export declare class GlinerBaseProvider implements DetectionProvider {
|
|
51
|
+
private _name;
|
|
52
|
+
get name(): string;
|
|
53
|
+
private env;
|
|
54
|
+
private tokenizer;
|
|
55
|
+
private session;
|
|
56
|
+
private loading;
|
|
57
|
+
private loadError;
|
|
58
|
+
private loadWaiters;
|
|
59
|
+
private progressCallback;
|
|
60
|
+
private threshold;
|
|
61
|
+
private customLabels;
|
|
62
|
+
private verification;
|
|
63
|
+
constructor(env: CoreEnv);
|
|
64
|
+
/**
|
|
65
|
+
* SHA-256 verification result of the served model blob (null until the
|
|
66
|
+
* model has been verified against the pinned hash). Surfaced so the
|
|
67
|
+
* model-status UI can show the positive "Model verified" state.
|
|
68
|
+
*/
|
|
69
|
+
getVerification(): ModelVerification | null;
|
|
70
|
+
isLoaded(): boolean;
|
|
71
|
+
isLoading(): boolean;
|
|
72
|
+
onProgress(callback: ProgressCallback): void;
|
|
73
|
+
setThreshold(value: number): void;
|
|
74
|
+
getThreshold(): number;
|
|
75
|
+
/** Get the combined list of all active labels (built-in + custom). */
|
|
76
|
+
private getActiveLabels;
|
|
77
|
+
/** Get user-defined custom labels. */
|
|
78
|
+
getCustomLabels(): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Set custom labels and persist through the injected KV store. The
|
|
81
|
+
* in-memory list updates synchronously (before the first await), so
|
|
82
|
+
* fire-and-forget callers keep the previous behavior.
|
|
83
|
+
*/
|
|
84
|
+
setCustomLabels(labels: string[]): Promise<void>;
|
|
85
|
+
/** Restore custom labels from the injected KV store on startup. */
|
|
86
|
+
restoreCustomLabels(): Promise<void>;
|
|
87
|
+
load(): Promise<void>;
|
|
88
|
+
release(): void;
|
|
89
|
+
detect(text: string, onProgress?: (progress: number) => void): Promise<DetectedEntity[]>;
|
|
90
|
+
private buildPromptTokens;
|
|
91
|
+
private inferChunk;
|
|
92
|
+
}
|