@doccloak/core 0.9.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.
@@ -0,0 +1,280 @@
1
+ /**
2
+ * @doccloak/core/dom - xlsx read/write (T110, file redaction phase 1).
3
+ *
4
+ * Minimal zip+XML implementation on the package's existing JSZip + DOMParser
5
+ * machinery (the same stack the docx module uses). SheetJS was considered and
6
+ * rejected at claim time: the community edition is Apache-2.0 but is a heavy
7
+ * dependency distributed outside npm since 0.18.x (vendor CDN), and DocCloak
8
+ * only needs string-cell rewriting, not formula evaluation or format
9
+ * conversion. The zip+XML path keeps the dependency surface unchanged.
10
+ *
11
+ * Text-bearing locations covered:
12
+ * - xl/sharedStrings.xml <si> entries (plain and rich-text runs, phonetic runs)
13
+ * - worksheet inline strings (<c t="inlineStr"><is>...)
14
+ * - worksheet cached formula string results (<c t="str"><v>)
15
+ * - worksheet header/footer definitions
16
+ * - legacy cell comments (xl/comments*.xml) and threaded comments
17
+ *
18
+ * Deliberately out of scope in phase 1 (documented for T-later): drawing/chart
19
+ * text parts (xl/drawings, xl/charts), data connection definitions
20
+ * (xl/connections.xml) and defined-name formulas. Metadata scrubbing follows
21
+ * the docx module's policy via the shared OPC helpers.
22
+ */
23
+ import JSZip from 'jszip';
24
+ import { applyTextReplacements } from "./docx.js";
25
+ import { readXmlPart, writeXmlPart, scrubDocPropsParts, removePackageThumbnail, scrubExternalRelTargets, replaceSensitiveValues, } from "./opc.js";
26
+ import { getFileExtension } from "./docx.js";
27
+ const SS_NS = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
28
+ function isSpreadsheetEl(el, localName) {
29
+ return el.namespaceURI === SS_NS && el.localName === localName;
30
+ }
31
+ /** Collect descendant <t> elements (spreadsheet namespace) in document order. */
32
+ function collectTextLeaves(root) {
33
+ const result = [];
34
+ const all = root.getElementsByTagName('*');
35
+ for (let i = 0; i < all.length; i++) {
36
+ if (isSpreadsheetEl(all[i], 't'))
37
+ result.push(all[i]);
38
+ }
39
+ return result;
40
+ }
41
+ /** String units in xl/sharedStrings.xml: one per <si>. */
42
+ function sharedStringUnits(xmlDoc) {
43
+ const units = [];
44
+ const sis = xmlDoc.getElementsByTagNameNS(SS_NS, 'si');
45
+ for (let i = 0; i < sis.length; i++) {
46
+ units.push(collectTextLeaves(sis[i]));
47
+ }
48
+ return units;
49
+ }
50
+ const HEADER_FOOTER_NAMES = new Set([
51
+ 'oddHeader', 'oddFooter', 'evenHeader', 'evenFooter', 'firstHeader', 'firstFooter',
52
+ ]);
53
+ /**
54
+ * String units in a worksheet: inline-string cells, cached string formula
55
+ * results and header/footer definitions, in document order.
56
+ */
57
+ function worksheetUnits(xmlDoc) {
58
+ const units = [];
59
+ const all = xmlDoc.getElementsByTagName('*');
60
+ for (let i = 0; i < all.length; i++) {
61
+ const el = all[i];
62
+ if (el.namespaceURI !== SS_NS)
63
+ continue;
64
+ if (el.localName === 'c') {
65
+ const type = el.getAttribute('t');
66
+ if (type === 'inlineStr') {
67
+ const leaves = collectTextLeaves(el);
68
+ if (leaves.length > 0)
69
+ units.push(leaves);
70
+ }
71
+ else if (type === 'str') {
72
+ // Cached formula result; the <f> formula itself is scrubbed by value.
73
+ for (let c = 0; c < el.children.length; c++) {
74
+ const child = el.children[c];
75
+ if (isSpreadsheetEl(child, 'v'))
76
+ units.push([child]);
77
+ }
78
+ }
79
+ }
80
+ else if (HEADER_FOOTER_NAMES.has(el.localName)) {
81
+ units.push([el]);
82
+ }
83
+ }
84
+ return units;
85
+ }
86
+ /**
87
+ * String units in a comments part. Legacy comments keep text in <t> leaves
88
+ * under each <comment><text>; threaded comments keep plain text in a <text>
89
+ * element (Microsoft threadedComments namespace, matched by local name).
90
+ */
91
+ function commentUnits(xmlDoc) {
92
+ const units = [];
93
+ const root = xmlDoc.documentElement;
94
+ if (root.localName === 'comments') {
95
+ const texts = xmlDoc.getElementsByTagNameNS(SS_NS, 'text');
96
+ for (let i = 0; i < texts.length; i++) {
97
+ const leaves = collectTextLeaves(texts[i]);
98
+ if (leaves.length > 0)
99
+ units.push(leaves);
100
+ }
101
+ }
102
+ else {
103
+ // threadedComments (or any other vendor comments schema): plain-text
104
+ // <text> leaves, matched leniently by local name.
105
+ const all = xmlDoc.getElementsByTagName('*');
106
+ for (let i = 0; i < all.length; i++) {
107
+ const el = all[i];
108
+ if (el.localName === 'text' && el.childElementCount === 0) {
109
+ units.push([el]);
110
+ }
111
+ }
112
+ }
113
+ return units;
114
+ }
115
+ /**
116
+ * Read a .xlsx file and extract its text content with position mapping.
117
+ */
118
+ export async function readXlsx(file) {
119
+ const arrayBuffer = await file.arrayBuffer();
120
+ const zip = await JSZip.loadAsync(arrayBuffer);
121
+ if (!zip.file('xl/workbook.xml')) {
122
+ throw new Error('Invalid .xlsx file: missing xl/workbook.xml');
123
+ }
124
+ // Deterministic part order so load-time and export-time extractions produce
125
+ // identical offsets: sharedStrings, then worksheets, then comment parts.
126
+ const worksheetPaths = [];
127
+ const commentPaths = [];
128
+ zip.forEach((relativePath) => {
129
+ if (/^xl\/worksheets\/[^/]+\.xml$/.test(relativePath)) {
130
+ worksheetPaths.push(relativePath);
131
+ }
132
+ else if (/^xl\/comments\d*\.xml$/.test(relativePath) ||
133
+ /^xl\/threadedComments\/[^/]+\.xml$/.test(relativePath)) {
134
+ commentPaths.push(relativePath);
135
+ }
136
+ });
137
+ worksheetPaths.sort();
138
+ commentPaths.sort();
139
+ const partPlans = [
140
+ { path: 'xl/sharedStrings.xml', unitsOf: sharedStringUnits },
141
+ ...worksheetPaths.map((path) => ({ path, unitsOf: worksheetUnits })),
142
+ ...commentPaths.map((path) => ({ path, unitsOf: commentUnits })),
143
+ ];
144
+ let flatText = '';
145
+ const contentParts = [];
146
+ for (const plan of partPlans) {
147
+ const xmlDoc = await readXmlPart(zip, plan.path);
148
+ if (!xmlDoc)
149
+ continue;
150
+ const textNodes = [];
151
+ for (const unit of plan.unitsOf(xmlDoc)) {
152
+ const nodes = [];
153
+ let unitLength = 0;
154
+ for (const element of unit) {
155
+ const text = element.textContent ?? '';
156
+ if (text.length === 0)
157
+ continue;
158
+ nodes.push({ element, flatStart: unitLength, flatEnd: unitLength + text.length });
159
+ unitLength += text.length;
160
+ }
161
+ if (unitLength === 0)
162
+ continue;
163
+ const base = flatText.length === 0 ? 0 : flatText.length + 1;
164
+ if (flatText.length > 0)
165
+ flatText += '\n';
166
+ for (const node of nodes) {
167
+ node.flatStart += base;
168
+ node.flatEnd += base;
169
+ flatText += node.element.textContent ?? '';
170
+ textNodes.push(node);
171
+ }
172
+ }
173
+ contentParts.push({ path: plan.path, xmlDoc, textNodes });
174
+ }
175
+ return {
176
+ plainText: flatText,
177
+ zip,
178
+ contentParts,
179
+ textNodes: contentParts.flatMap((part) => part.textNodes),
180
+ };
181
+ }
182
+ /**
183
+ * Scrub identity-bearing attributes inside a worksheet or comments part:
184
+ * hyperlink display/tooltip strings, legacy comment author registry, and
185
+ * literal strings inside formulas (<f>), which offset replacement cannot
186
+ * reach because formulas are not part of the flat text.
187
+ */
188
+ function sanitizeXlsxPart(xmlDoc, valueReplacements) {
189
+ const all = xmlDoc.getElementsByTagName('*');
190
+ for (let i = 0; i < all.length; i++) {
191
+ const el = all[i];
192
+ if (el.localName === 'hyperlink') {
193
+ for (const attr of ['display', 'tooltip', 'location']) {
194
+ const value = el.getAttribute(attr);
195
+ if (value)
196
+ el.setAttribute(attr, replaceSensitiveValues(value, valueReplacements, false));
197
+ }
198
+ }
199
+ else if (isSpreadsheetEl(el, 'f')) {
200
+ const formula = el.textContent ?? '';
201
+ const scrubbed = replaceSensitiveValues(formula, valueReplacements, false);
202
+ if (scrubbed !== formula)
203
+ el.textContent = scrubbed;
204
+ }
205
+ else if (el.localName === 'author' && el.parentElement?.localName === 'authors') {
206
+ // Legacy comments author registry (indexes must stay stable)
207
+ el.textContent = 'Redacted';
208
+ }
209
+ }
210
+ }
211
+ /**
212
+ * Scrub metadata parts that survive text replacement untouched: docProps,
213
+ * the package thumbnail, the workbook's absolute-path fingerprint and
214
+ * file-sharing identity, the threaded-comment person registry, and external
215
+ * relationship targets (hyperlink mailto:/URLs).
216
+ */
217
+ async function sanitizeXlsxMetadata(zip, valueReplacements) {
218
+ await scrubDocPropsParts(zip);
219
+ await removePackageThumbnail(zip);
220
+ // Workbook: x15ac:absPath records the full local path the file was saved
221
+ // from (user name, client folders); fileSharing records the reserving user.
222
+ const workbook = await readXmlPart(zip, 'xl/workbook.xml');
223
+ if (workbook) {
224
+ const all = workbook.getElementsByTagName('*');
225
+ const doomed = [];
226
+ for (let i = 0; i < all.length; i++) {
227
+ if (all[i].localName === 'absPath' || all[i].localName === 'fileSharing') {
228
+ doomed.push(all[i]);
229
+ }
230
+ }
231
+ for (const el of doomed)
232
+ el.parentNode?.removeChild(el);
233
+ writeXmlPart(zip, 'xl/workbook.xml', workbook);
234
+ }
235
+ // Threaded-comment person registry: display names and provider user ids
236
+ const personsPaths = [];
237
+ zip.forEach((relativePath) => {
238
+ if (/^xl\/persons\/[^/]+\.xml$/.test(relativePath))
239
+ personsPaths.push(relativePath);
240
+ });
241
+ for (const path of personsPaths.sort()) {
242
+ const persons = await readXmlPart(zip, path);
243
+ if (!persons)
244
+ continue;
245
+ const all = persons.getElementsByTagName('*');
246
+ for (let i = 0; i < all.length; i++) {
247
+ const attrs = all[i].attributes;
248
+ for (let a = 0; a < attrs.length; a++) {
249
+ if (attrs[a].localName === 'displayName' || attrs[a].localName === 'userId') {
250
+ attrs[a].value = 'Redacted';
251
+ }
252
+ }
253
+ }
254
+ writeXmlPart(zip, path, persons);
255
+ }
256
+ await scrubExternalRelTargets(zip, /^xl\/(?:[^/]+\/)*_rels\/[^/]+\.rels$/, valueReplacements);
257
+ }
258
+ /**
259
+ * Apply text replacements to the xlsx XML, preserving formatting, then scrub
260
+ * metadata. Returns a new .xlsx file as a Blob; the input file is untouched.
261
+ */
262
+ export async function writeAnonymizedXlsx(extraction, replacements, valueReplacements = []) {
263
+ applyTextReplacements(extraction.textNodes, replacements);
264
+ for (const part of extraction.contentParts) {
265
+ sanitizeXlsxPart(part.xmlDoc, valueReplacements);
266
+ writeXmlPart(extraction.zip, part.path, part.xmlDoc);
267
+ }
268
+ await sanitizeXlsxMetadata(extraction.zip, valueReplacements);
269
+ return extraction.zip.generateAsync({
270
+ type: 'blob',
271
+ mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
272
+ });
273
+ }
274
+ /**
275
+ * Check if a file is a supported Excel workbook (.xlsx only; the legacy
276
+ * binary .xls format is not supported in phase 1).
277
+ */
278
+ export function isExcelFile(filename) {
279
+ return getFileExtension(filename) === 'xlsx';
280
+ }
package/dist/engine.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  import type { CoreEnv, HardwareHints } from './env.js';
17
17
  import type { DetectedEntity, DetectionProvider } from './types.js';
18
18
  import { type RegexRegionId } from './regex/index.js';
19
- export type ProviderId = 'gliner' | 'bardsai';
19
+ export type ProviderId = 'gliner' | 'gliner-base' | 'bardsai';
20
20
  export interface ProviderEntry {
21
21
  id: ProviderId;
22
22
  label: string;
@@ -72,10 +72,13 @@ export declare function defaultThresholdFor(id: ProviderId): number;
72
72
  export declare function clampThreshold(value: number): number;
73
73
  /**
74
74
  * Default-model heuristic for devices without a saved provider choice.
75
- * The BardS.ai model (~279 MB download, ~500+ MB peak RAM while loading)
76
- * routinely gets the tab killed on mobile browsers, so constrained devices
77
- * default to the lightweight GLiNER model (~65 MB). Hosts feed the hints
78
- * (userAgent/deviceMemory sniffing stays host-side).
75
+ * Large models (~200-500+ MB peak RAM while loading) routinely get the tab
76
+ * killed on mobile browsers, so constrained devices default to the
77
+ * lightweight GLiNER Small model (~83 MB). Desktop-class devices default
78
+ * to GLiNER PII Base (~197 MB, higher accuracy, zero-shot custom labels;
79
+ * T122 - previously BardS.ai, which stays selectable as legacy). A saved
80
+ * provider choice always wins over this heuristic (existing behavior).
81
+ * Hosts feed the hints (userAgent/deviceMemory sniffing stays host-side).
79
82
  */
80
83
  export declare function pickDefaultProvider(hardware?: HardwareHints): ProviderId;
81
84
  export declare function createEngine(env: CoreEnv, initial?: Partial<EngineSettings>, options?: EngineOptions): DocCloakEngine;
package/dist/engine.js CHANGED
@@ -15,19 +15,25 @@
15
15
  */
16
16
  import { detectWithRegex, detectEntities } from "./pipeline.js";
17
17
  import { GlinerProvider } from "./providers/gliner.js";
18
+ import { GlinerBaseProvider } from "./providers/gliner-base.js";
18
19
  import { BardsaiProvider } from "./providers/bardsai.js";
19
20
  import { REGEX_REGIONS } from "./regex/index.js";
20
21
  /** Catalog of the built-in providers (labels/descriptions shown by hosts). */
21
22
  export const PROVIDERS = [
22
23
  {
23
24
  id: 'gliner',
24
- label: 'GLiNER PII Edge',
25
- description: 'Lightweight, multi-language, supports custom labels (~65 MB)',
25
+ label: 'GLiNER PII Small',
26
+ description: 'Lightweight, multi-language, supports custom labels (~83 MB)',
27
+ },
28
+ {
29
+ id: 'gliner-base',
30
+ label: 'GLiNER PII Base',
31
+ description: 'May be better for English and dates, supports custom labels (~197 MB)',
26
32
  },
27
33
  {
28
34
  id: 'bardsai',
29
35
  label: 'BardS.ai EU PII',
30
- description: '24 EU languages, 35 PII types, high accuracy (~279 MB)',
36
+ description: 'Best multilingual accuracy, 24+ EU languages, 35 PII types (~279 MB)',
31
37
  },
32
38
  ];
33
39
  // ── Settings ───────────────────────────────────────────────
@@ -53,16 +59,23 @@ export function clampThreshold(value) {
53
59
  }
54
60
  /**
55
61
  * Default-model heuristic for devices without a saved provider choice.
56
- * The BardS.ai model (~279 MB download, ~500+ MB peak RAM while loading)
57
- * routinely gets the tab killed on mobile browsers, so constrained devices
58
- * default to the lightweight GLiNER model (~65 MB). Hosts feed the hints
59
- * (userAgent/deviceMemory sniffing stays host-side).
62
+ * Large models (~200-500+ MB peak RAM while loading) routinely get the tab
63
+ * killed on mobile browsers, so constrained devices default to the
64
+ * lightweight GLiNER Small model (~83 MB). Desktop-class devices default
65
+ * to GLiNER PII Base (~197 MB, higher accuracy, zero-shot custom labels;
66
+ * T122 - previously BardS.ai, which stays selectable as legacy). A saved
67
+ * provider choice always wins over this heuristic (existing behavior).
68
+ * Hosts feed the hints (userAgent/deviceMemory sniffing stays host-side).
60
69
  */
61
70
  export function pickDefaultProvider(hardware) {
62
71
  if (hardware?.isMobile)
63
72
  return 'gliner';
64
73
  if (typeof hardware?.deviceMemoryGB === 'number' && hardware.deviceMemoryGB <= 4)
65
74
  return 'gliner';
75
+ // T127: the 2026-08 benchmark (documentation/model-comparison-2026-08.md)
76
+ // showed bardsai is the strongest and most uniform across languages, so it
77
+ // is the capable-device default again; gliner-base stays selectable as the
78
+ // English/dates/custom-labels alternative.
66
79
  return 'bardsai';
67
80
  }
68
81
  function isProviderId(value) {
@@ -78,6 +91,7 @@ function supportsCustomLabels(p) {
78
91
  export function createEngine(env, initial, options) {
79
92
  const factories = {
80
93
  gliner: (e) => new GlinerProvider(e),
94
+ 'gliner-base': (e) => new GlinerBaseProvider(e),
81
95
  bardsai: (e) => new BardsaiProvider(e),
82
96
  ...options?.providers,
83
97
  };
package/dist/index.d.ts CHANGED
@@ -14,6 +14,8 @@ export * from './doc.js';
14
14
  export { generateSurrogate, generateUniqueSurrogate, generateSessionSalt, sessionDayOffset, FAKE_EMAIL_DOMAINS, } from './surrogates.js';
15
15
  export type { SurrogateContext } from './surrogates.js';
16
16
  export { GlinerProvider, GLINER_MODEL_URL, GLINER_MODEL_REVISION, GLINER_MODEL_SHA256 } from './providers/gliner.js';
17
+ export { GlinerBaseProvider, GLINER_BASE_MODEL_URL, GLINER_BASE_MODEL_REVISION, GLINER_BASE_MODEL_SHA256, GLINER_BASE_MAX_WIDTH, buildSpanIndices, decodeSpanLogits, } from './providers/gliner-base.js';
18
+ export type { SpanIndices } from './providers/gliner-base.js';
17
19
  export { BardsaiProvider, BARDSAI_MODEL_URL, BARDSAI_MODEL_REVISION, BARDSAI_MODEL_SHA256 } from './providers/bardsai.js';
18
20
  export { detectWithRegex, resolveOverlaps, filterFalsePositives, propagateEntities, detectEntities, } from './pipeline.js';
19
21
  export { createEngine, PROVIDERS, pickDefaultProvider, defaultThresholdFor, clampThreshold, ENGINE_SETTINGS_KEYS, } from './engine.js';
package/dist/index.js CHANGED
@@ -20,6 +20,9 @@ export { generateSurrogate, generateUniqueSurrogate, generateSessionSalt, sessio
20
20
  // T007: gliner provider
21
21
  export { GlinerProvider, GLINER_MODEL_URL, GLINER_MODEL_REVISION, GLINER_MODEL_SHA256 } from "./providers/gliner.js";
22
22
  // end T007
23
+ // T122: gliner base provider (markerV0 span mode)
24
+ export { GlinerBaseProvider, GLINER_BASE_MODEL_URL, GLINER_BASE_MODEL_REVISION, GLINER_BASE_MODEL_SHA256, GLINER_BASE_MAX_WIDTH, buildSpanIndices, decodeSpanLogits, } from "./providers/gliner-base.js";
25
+ // end T122
23
26
  // T008: bardsai provider
24
27
  export { BardsaiProvider, BARDSAI_MODEL_URL, BARDSAI_MODEL_REVISION, BARDSAI_MODEL_SHA256 } from "./providers/bardsai.js";
25
28
  // end T008
@@ -10,7 +10,10 @@
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
- import * as ort from 'onnxruntime-web';
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';
14
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
@@ -231,13 +234,11 @@ export class BardsaiProvider {
231
234
  }
232
235
  if (words.length === 0)
233
236
  return [];
234
- // 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.
235
240
  onProgress?.(0);
236
- const fullEncoded = await this.tokenizer(words.map((w) => w.word).join(' '), {
237
- add_special_tokens: false,
238
- });
239
- const tokenIds = Array.from(fullEncoded.input_ids.data);
240
- const tokenStrings = this.tokenizer.model.convert_ids_to_tokens(tokenIds.map(Number));
241
+ const tokenStrings = this.tokenizer.tokenize(words.map((w) => w.word).join(' '));
241
242
  // Count subtokens per word: ▁ prefix marks word boundaries (SentencePiece)
242
243
  const subtokenCounts = [];
243
244
  let currentCount = 0;
@@ -317,16 +318,15 @@ export class BardsaiProvider {
317
318
  const attentionMask = encoded.attention_mask.data;
318
319
  const seqLen = encoded.input_ids.dims[1];
319
320
  // Build word→subtoken alignment via ▁ prefix (no per-word tokenization needed)
320
- const chunkTokenIds = Array.from(inputIds);
321
- const chunkTokenStrings = this.tokenizer.model.convert_ids_to_tokens(chunkTokenIds.map(Number));
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);
322
326
  const wordSubtokenStart = [];
323
- // Skip <s> at position 0; first real token at position 1
324
- for (let t = 1; t < seqLen - 1; t++) {
325
- const tok = chunkTokenStrings[t];
326
- if (!tok || tok === '</s>')
327
- break;
328
- if (tok.startsWith('\u2581')) {
329
- 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);
330
330
  }
331
331
  }
332
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
+ }