@stabrise/scaledp 0.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.
Files changed (60) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +218 -0
  3. package/dist/box-DAfzwfhA.d.ts +119 -0
  4. package/dist/config-g6IrKlDC.d.ts +80 -0
  5. package/dist/data-to-image-DoZ4jQ3R.js +54 -0
  6. package/dist/data-to-image-DoZ4jQ3R.js.map +1 -0
  7. package/dist/detect/index.d.ts +71 -0
  8. package/dist/detect/index.js +2 -0
  9. package/dist/detect-q8AI_Jdj.js +274 -0
  10. package/dist/detect-q8AI_Jdj.js.map +1 -0
  11. package/dist/detector-output-C0Qt-jEq.d.ts +13 -0
  12. package/dist/detector-output-lyF1Mqb8.js +13 -0
  13. package/dist/detector-output-lyF1Mqb8.js.map +1 -0
  14. package/dist/display/index.d.ts +66 -0
  15. package/dist/display/index.js +237 -0
  16. package/dist/display/index.js.map +1 -0
  17. package/dist/document-B8I61TiY.d.ts +16 -0
  18. package/dist/entity-CedtRhU1.d.ts +22 -0
  19. package/dist/entity-D6Hxaugj.js +13 -0
  20. package/dist/entity-D6Hxaugj.js.map +1 -0
  21. package/dist/image-CAH2rLv9.js +511 -0
  22. package/dist/image-CAH2rLv9.js.map +1 -0
  23. package/dist/image-Dc5TSg46.d.ts +18 -0
  24. package/dist/image-DoZDJkcR.js +37 -0
  25. package/dist/image-DoZDJkcR.js.map +1 -0
  26. package/dist/image-draw-boxes-De0QbFv9.js +285 -0
  27. package/dist/image-draw-boxes-De0QbFv9.js.map +1 -0
  28. package/dist/index.d.ts +269 -0
  29. package/dist/index.js +11 -0
  30. package/dist/model-cache-BEaqqRZ9.js +182 -0
  31. package/dist/model-cache-BEaqqRZ9.js.map +1 -0
  32. package/dist/model-cache-BhFYpfZz.d.ts +36 -0
  33. package/dist/ner/index.d.ts +293 -0
  34. package/dist/ner/index.js +2 -0
  35. package/dist/ner-SsZLZ6ed.js +1028 -0
  36. package/dist/ner-SsZLZ6ed.js.map +1 -0
  37. package/dist/ocr/index.d.ts +440 -0
  38. package/dist/ocr/index.js +3 -0
  39. package/dist/ocr-OHX2WM3e.js +1294 -0
  40. package/dist/ocr-OHX2WM3e.js.map +1 -0
  41. package/dist/ort-CXDoPrtw.js +73 -0
  42. package/dist/ort-CXDoPrtw.js.map +1 -0
  43. package/dist/params-DapwK9Ns.js +37 -0
  44. package/dist/params-DapwK9Ns.js.map +1 -0
  45. package/dist/pdf/index.d.ts +123 -0
  46. package/dist/pdf/index.js +2 -0
  47. package/dist/pdf-BQl0dneD.js +417 -0
  48. package/dist/pdf-BQl0dneD.js.map +1 -0
  49. package/dist/pipeline-DACqGkpN.js +240 -0
  50. package/dist/pipeline-DACqGkpN.js.map +1 -0
  51. package/dist/pipeline-DeLO-OCE.d.ts +139 -0
  52. package/dist/registry/index.d.ts +169 -0
  53. package/dist/registry/index.js +1061 -0
  54. package/dist/registry/index.js.map +1 -0
  55. package/dist/text-ahMLpxN9.js +109 -0
  56. package/dist/text-ahMLpxN9.js.map +1 -0
  57. package/dist/worker/index.d.ts +105 -0
  58. package/dist/worker/index.js +180 -0
  59. package/dist/worker/index.js.map +1 -0
  60. package/package.json +135 -0
@@ -0,0 +1,1028 @@
1
+ import { h as getConfig, i as Stage, l as NerError } from "./pipeline-DACqGkpN.js";
2
+ import { n as ensureModelFiles } from "./model-cache-BEaqqRZ9.js";
3
+ import { i as resolveParams, t as BASE_STAGE_DEFAULTS } from "./params-DapwK9Ns.js";
4
+ import { t as createNerOutput } from "./entity-D6Hxaugj.js";
5
+ import { t as createSession } from "./ort-CXDoPrtw.js";
6
+ //#region src/ner/chunking.ts
7
+ /** Python's `split_text` default, and what the cloud /ner/text endpoint uses. */
8
+ const DEFAULT_CHUNK_LENGTH = 500;
9
+ /** 500 - 480 leaves a 20-character overlap so entities on a seam survive. */
10
+ const DEFAULT_CHUNK_STRIDE = 480;
11
+ function chunkText(text, maxLength = 500, stride = 480) {
12
+ if (stride <= 0) throw new RangeError(`stride must be positive, received ${stride}`);
13
+ if (maxLength <= 0) throw new RangeError(`maxLength must be positive, received ${maxLength}`);
14
+ if (text.length === 0) return [];
15
+ if (text.length <= maxLength) return [{
16
+ text,
17
+ offset: 0
18
+ }];
19
+ const chunks = [];
20
+ for (let offset = 0; offset < text.length; offset += stride) {
21
+ const slice = text.slice(offset, offset + maxLength);
22
+ chunks.push({
23
+ text: slice,
24
+ offset
25
+ });
26
+ if (slice.length < maxLength) break;
27
+ }
28
+ return chunks;
29
+ }
30
+ /** Shift a chunk-local span onto the original text's coordinates. */
31
+ function rebaseSpan(span, offset) {
32
+ return {
33
+ ...span,
34
+ start: span.start + offset,
35
+ end: span.end + offset
36
+ };
37
+ }
38
+ /**
39
+ * Drop duplicates produced by the chunk overlap, keeping the highest score for
40
+ * each distinct (start, end, label).
41
+ */
42
+ function dedupeSpans(spans) {
43
+ const best = /* @__PURE__ */ new Map();
44
+ for (const span of spans) {
45
+ const key = `${span.start}:${span.end}:${span.label}`;
46
+ const existing = best.get(key);
47
+ if (!existing || span.score > existing.score) best.set(key, span);
48
+ }
49
+ return [...best.values()].sort((a, b) => a.start - b.start);
50
+ }
51
+ /** Ratio of uppercase to cased letters. */
52
+ function uppercaseRatio(text) {
53
+ let upper = 0;
54
+ let lower = 0;
55
+ for (const char of text) if (char >= "A" && char <= "Z") upper++;
56
+ else if (char >= "a" && char <= "z") lower++;
57
+ else if (char !== char.toLowerCase()) upper++;
58
+ else if (char !== char.toUpperCase()) lower++;
59
+ return upper + lower === 0 ? 0 : upper / (upper + lower);
60
+ }
61
+ const ALL_CAPS_RATIO = .6;
62
+ function isMostlyUppercase(text) {
63
+ return uppercaseRatio(text) > ALL_CAPS_RATIO;
64
+ }
65
+ /**
66
+ * Title-case runs of capitals, preserving length.
67
+ *
68
+ * GLiNER1 models are cased and scanned documents are frequently set in all
69
+ * caps, which reads to the model as unlike anything in training. Length
70
+ * preservation is essential: every character offset the decoder returns is used
71
+ * to index the original text.
72
+ */
73
+ function titleCaseAllCapsWords(text) {
74
+ return text.replace(/\p{Lu}[\p{Lu}\p{N}'’-]*\p{Lu}/gu, (word) => {
75
+ const titled = word[0] + word.slice(1).toLowerCase();
76
+ return titled.length === word.length ? titled : word;
77
+ });
78
+ }
79
+ /** Apply the casing fix only when the text is predominantly uppercase. */
80
+ function normaliseCasing(text) {
81
+ return isMostlyUppercase(text) ? titleCaseAllCapsWords(text) : text;
82
+ }
83
+ //#endregion
84
+ //#region src/ner/vendor/math.ts
85
+ /** Numeric helpers for the GLiNER runtimes. Adapted from @lmoe/gliner-onnx (MIT). */
86
+ /** Numerically stable sigmoid: exp(-x) overflows for large negative x. */
87
+ function sigmoid(x) {
88
+ if (x >= 0) return 1 / (1 + Math.exp(-x));
89
+ const expX = Math.exp(x);
90
+ return expX / (1 + expX);
91
+ }
92
+ function softmax(values) {
93
+ let max = Number.NEGATIVE_INFINITY;
94
+ for (let i = 0; i < values.length; i++) {
95
+ const v = values[i];
96
+ if (v > max) max = v;
97
+ }
98
+ const out = new Float32Array(values.length);
99
+ let sum = 0;
100
+ for (let i = 0; i < values.length; i++) {
101
+ const e = Math.exp(values[i] - max);
102
+ out[i] = e;
103
+ sum += e;
104
+ }
105
+ for (let i = 0; i < out.length; i++) out[i] = out[i] / sum;
106
+ return out;
107
+ }
108
+ /** Gather rows out of a flat [n, hiddenSize] matrix. */
109
+ function gatherRows(source, positions, hiddenSize) {
110
+ const out = new Float32Array(positions.length * hiddenSize);
111
+ for (let i = 0; i < positions.length; i++) {
112
+ const from = positions[i] * hiddenSize;
113
+ out.set(source.subarray(from, from + hiddenSize), i * hiddenSize);
114
+ }
115
+ return out;
116
+ }
117
+ /** Contiguous row slice of a flat [n, hiddenSize] matrix. */
118
+ function sliceRows(source, startRow, rowCount, hiddenSize) {
119
+ const from = startRow * hiddenSize;
120
+ return source.slice(from, from + rowCount * hiddenSize);
121
+ }
122
+ /** Token ids from a transformers.js `tolist()` result, flattened and de-bigint'd. */
123
+ function extractTokenIds(tolistResult) {
124
+ return (Array.isArray(tolistResult[0]) ? tolistResult.flat() : tolistResult).map((v) => typeof v === "bigint" ? Number(v) : v);
125
+ }
126
+ //#endregion
127
+ //#region src/ner/tokenizer-types.ts
128
+ /** Shared tokenizer adaptation, kept separate so backends need not import the loader. */
129
+ /**
130
+ * Adapt a transformers.js tokenizer to the span processor's interface.
131
+ *
132
+ * CLS/SEP ids are derived empirically -- encode a throwaway token, read the
133
+ * first and last id -- rather than read from `cls_token_id`. Not every GLiNER
134
+ * repo populates those fields, and a wrong id corrupts every sequence silently
135
+ * instead of failing loudly.
136
+ */
137
+ function toSpanTokenizer(tokenizer) {
138
+ const encode = (text) => extractTokenIds(tokenizer(text, { add_special_tokens: true }).input_ids.tolist());
139
+ const probe = encode("x");
140
+ if (probe.length < 2) throw new NerError("Tokenizer produced no special tokens; cannot derive CLS/SEP ids", "toSpanTokenizer");
141
+ return {
142
+ encode,
143
+ clsTokenId: probe[0],
144
+ sepTokenId: probe[probe.length - 1]
145
+ };
146
+ }
147
+ //#endregion
148
+ //#region src/ner/vendor/span-decoder.ts
149
+ /**
150
+ * GLiNER1 span decoder. Adapted from @lmoe/gliner-onnx (MIT).
151
+ *
152
+ * The model emits logits shaped [batch, seqLen, maxWidth, entityCount]. Every
153
+ * (start word, width, label) triple above threshold becomes a candidate span;
154
+ * greedy non-maximum suppression then keeps the highest-scoring
155
+ * non-overlapping set.
156
+ */
157
+ /** Model class ids are 1-based; index 0 is reserved. */
158
+ const ENTITY_ID_OFFSET = 1;
159
+ function spansOverlap(aStart, aEnd, bStart, bEnd, allowNested, allowMultiLabel) {
160
+ if (aStart === bStart && aEnd === bEnd) return !allowMultiLabel;
161
+ if (aStart > bEnd || bStart > aEnd) return false;
162
+ if (allowNested) {
163
+ if (aStart <= bStart && aEnd >= bEnd || bStart <= aStart && bEnd >= aEnd) return false;
164
+ }
165
+ return true;
166
+ }
167
+ /** Keep the highest-scoring spans that do not collide. */
168
+ function greedySearch(spans, flatNer, multiLabel) {
169
+ const byScore = [...spans].sort((a, b) => b.score - a.score);
170
+ const kept = [];
171
+ for (const span of byScore) if (!kept.some((other) => spansOverlap(span.start, span.end, other.start, other.end, !flatNer, multiLabel))) kept.push(span);
172
+ return kept.sort((a, b) => a.start - b.start);
173
+ }
174
+ function decodeSpans(logits, params, options = {}) {
175
+ const threshold = options.threshold ?? .5;
176
+ const flatNer = options.flatNer ?? true;
177
+ const multiLabel = options.multiLabel ?? false;
178
+ const { batchSize, inputLength, maxWidth, entityCount } = params;
179
+ const batchStride = inputLength * maxWidth * entityCount;
180
+ const tokenStride = maxWidth * entityCount;
181
+ const spans = Array.from({ length: batchSize }, () => []);
182
+ for (let index = 0; index < logits.length; index++) {
183
+ const score = sigmoid(logits[index]);
184
+ if (score < threshold) continue;
185
+ const batchIdx = Math.floor(index / batchStride);
186
+ const startWord = Math.floor(index / tokenStride) % inputLength;
187
+ const endWord = startWord + Math.floor(index / entityCount) % maxWidth;
188
+ const entityIdx = index % entityCount;
189
+ const words = params.batchWords[batchIdx];
190
+ if (!words || startWord >= words.length || endWord >= words.length) continue;
191
+ const start = words[startWord][1];
192
+ const end = words[endWord][2];
193
+ const text = params.texts[batchIdx] ?? "";
194
+ spans[batchIdx].push({
195
+ text: text.slice(start, end),
196
+ label: params.idToClass[entityIdx + ENTITY_ID_OFFSET] ?? "",
197
+ start,
198
+ end,
199
+ score
200
+ });
201
+ }
202
+ return spans.map((batch) => greedySearch(batch, flatNer, multiLabel));
203
+ }
204
+ //#endregion
205
+ //#region src/ner/vendor/splitter.ts
206
+ /** Word splitting for GLiNER. Adapted from @lmoe/gliner-onnx (MIT). */
207
+ /**
208
+ * Unicode word classes (`\p{L}\p{N}` with the `u` flag), deliberately not `\w`.
209
+ *
210
+ * JavaScript's `\w` is ASCII-only even under `/u`, so it would shatter accented
211
+ * names -- "Müller", "García" -- into single-character tokens and wreck
212
+ * multi-word span detection in German, Polish and Spanish text. Python's `\w`
213
+ * is Unicode-aware, so this restores parity with the reference tokenizer.
214
+ */
215
+ const WORD_PATTERN = /[\p{L}\p{N}_]+(?:[-_][\p{L}\p{N}_]+)*|\S/gu;
216
+ /**
217
+ * As above, plus leading branches for URLs, emails and @mentions so they stay
218
+ * whole. Those branches are ASCII on purpose, mirroring the Python pattern.
219
+ */
220
+ const RICH_WORD_PATTERN = /(?:https?:\/\/[^\s]+|www\.[^\s]+)|[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}|@[a-z0-9_]+|[\p{L}\p{N}_]+(?:[-_][\p{L}\p{N}_]+)*|\S/giu;
221
+ /**
222
+ * Split text into words with their character offsets.
223
+ *
224
+ * A fresh RegExp per call: the `g` flag makes `lastIndex` stateful, so sharing
225
+ * one instance across calls silently skips matches.
226
+ */
227
+ function splitWords(text, pattern = WORD_PATTERN) {
228
+ const regex = new RegExp(pattern.source, pattern.flags);
229
+ const out = [];
230
+ for (;;) {
231
+ const match = regex.exec(text);
232
+ if (match === null) break;
233
+ out.push([
234
+ match[0],
235
+ match.index,
236
+ regex.lastIndex
237
+ ]);
238
+ }
239
+ return out;
240
+ }
241
+ //#endregion
242
+ //#region src/ner/vendor/span-processor.ts
243
+ /**
244
+ * GLiNER1 span-enumeration input builder. Adapted from @lmoe/gliner-onnx (MIT).
245
+ *
246
+ * Builds the prompt `<<ENT>> label1 <<ENT>> label2 <<SEP>> text` and enumerates
247
+ * every candidate span up to `maxWidth` words, which is what the model scores.
248
+ */
249
+ const PAD = 0;
250
+ function pad(arrays, value) {
251
+ const max = Math.max(...arrays.map((a) => a.length));
252
+ return arrays.map((a) => [...a, ...new Array(max - a.length).fill(value)]);
253
+ }
254
+ var SpanProcessor = class {
255
+ config;
256
+ tokenizer;
257
+ constructor(config, tokenizer) {
258
+ this.config = config;
259
+ this.tokenizer = tokenizer;
260
+ }
261
+ prepare(texts, labels) {
262
+ const batchWords = texts.map((text) => splitWords(text));
263
+ const idToClass = {};
264
+ for (const [i, label] of labels.entries()) idToClass[i + 1] = label;
265
+ const inputIds = [];
266
+ const attentionMasks = [];
267
+ const wordsMasks = [];
268
+ const textLengths = [];
269
+ const spanIdxs = [];
270
+ const spanMasks = [];
271
+ for (const words of batchWords) {
272
+ textLengths.push(words.length);
273
+ const prompt = [];
274
+ for (const label of labels) prompt.push(this.config.entToken, label);
275
+ prompt.push(this.config.sepToken);
276
+ const sequence = [...prompt, ...words.map(([w]) => w)];
277
+ const ids = [this.tokenizer.clsTokenId];
278
+ const attention = [1];
279
+ const wordsMask = [PAD];
280
+ let wordCounter = 1;
281
+ for (const [wordIdx, word] of sequence.entries()) {
282
+ const subTokens = this.tokenizer.encode(word).slice(1, -1);
283
+ for (const [tokenIdx, id] of subTokens.entries()) {
284
+ ids.push(id);
285
+ attention.push(1);
286
+ if (wordIdx < prompt.length) wordsMask.push(PAD);
287
+ else if (tokenIdx === 0) wordsMask.push(wordCounter++);
288
+ else wordsMask.push(PAD);
289
+ }
290
+ }
291
+ ids.push(this.tokenizer.sepTokenId);
292
+ attention.push(1);
293
+ wordsMask.push(PAD);
294
+ inputIds.push(ids);
295
+ attentionMasks.push(attention);
296
+ wordsMasks.push(wordsMask);
297
+ const spanIdx = [];
298
+ const spanMask = [];
299
+ for (let start = 0; start < words.length; start++) for (let width = 0; width < this.config.maxWidth; width++) {
300
+ const end = Math.min(start + width, words.length - 1);
301
+ spanIdx.push([start, end]);
302
+ spanMask.push(end < words.length);
303
+ }
304
+ spanIdxs.push(spanIdx);
305
+ spanMasks.push(spanMask);
306
+ }
307
+ const maxSpans = Math.max(...spanIdxs.map((s) => s.length));
308
+ return {
309
+ inputIds: pad(inputIds, PAD),
310
+ attentionMasks: pad(attentionMasks, PAD),
311
+ wordsMasks: pad(wordsMasks, PAD),
312
+ textLengths,
313
+ spanIdxs: spanIdxs.map((s) => [...s, ...Array.from({ length: maxSpans - s.length }, () => [PAD, PAD])]),
314
+ spanMasks: pad(spanMasks, false),
315
+ idToClass,
316
+ batchWords
317
+ };
318
+ }
319
+ };
320
+ //#endregion
321
+ //#region src/ner/gliner1-backend.ts
322
+ /**
323
+ * GLiNER1 span-enumeration runtime on onnxruntime-web.
324
+ *
325
+ * One ONNX graph. Inputs:
326
+ * input_ids int64 [batch, tokens]
327
+ * attention_mask int64 [batch, tokens]
328
+ * words_mask int64 [batch, tokens] 1-based word index on first sub-token
329
+ * text_lengths int64 [batch, 1]
330
+ * span_idx int64 [batch, spans, 2]
331
+ * span_mask bool [batch, spans]
332
+ * Output:
333
+ * logits float32 [batch, words, maxWidth, entityCount]
334
+ */
335
+ /** Fallback when a repo's config omits max_width. */
336
+ const DEFAULT_MAX_WIDTH = 12;
337
+ const DEFAULT_ENT_TOKEN = "<<ENT>>";
338
+ const DEFAULT_SEP_TOKEN = "<<SEP>>";
339
+ const CONFIG_CANDIDATES = ["gliner_config.json", "config.json"];
340
+ /** Some repos put the graph at the root rather than under onnx/. */
341
+ const MODEL_EXTENSIONS = [".onnx", ".model"];
342
+ const OUTPUT_LOGITS = "logits";
343
+ function readConfig(files) {
344
+ for (const name of CONFIG_CANDIDATES) {
345
+ const raw = files[name];
346
+ if (!raw) continue;
347
+ try {
348
+ const parsed = JSON.parse(new TextDecoder().decode(raw));
349
+ return {
350
+ maxWidth: Number(parsed.max_width) || DEFAULT_MAX_WIDTH,
351
+ entToken: String(parsed.ent_token ?? DEFAULT_ENT_TOKEN),
352
+ sepToken: String(parsed.sep_token ?? DEFAULT_SEP_TOKEN)
353
+ };
354
+ } catch {}
355
+ }
356
+ return {
357
+ maxWidth: DEFAULT_MAX_WIDTH,
358
+ entToken: DEFAULT_ENT_TOKEN,
359
+ sepToken: DEFAULT_SEP_TOKEN
360
+ };
361
+ }
362
+ function findModelBytes(files) {
363
+ for (const [path, bytes] of Object.entries(files)) if (MODEL_EXTENSIONS.some((ext) => path.endsWith(ext))) return bytes;
364
+ throw new NerError("No .onnx or .model file in the downloaded model", "Gliner1Backend");
365
+ }
366
+ var Gliner1Backend = class {
367
+ arch = "gliner1";
368
+ session = null;
369
+ processor = null;
370
+ config = null;
371
+ async load(files, options) {
372
+ this.config = readConfig(files);
373
+ this.processor = new SpanProcessor(this.config, toSpanTokenizer(options.tokenizer));
374
+ this.session = await createSession(findModelBytes(files), { executionProviders: options.executionProviders });
375
+ }
376
+ async extract(text, labels, threshold) {
377
+ const session = this.session;
378
+ const processor = this.processor;
379
+ const config = this.config;
380
+ if (!session || !processor || !config) throw new NerError("Backend used before load()", "Gliner1Backend");
381
+ if (labels.length === 0) return [];
382
+ const batch = processor.prepare([text], labels);
383
+ const words = batch.batchWords[0] ?? [];
384
+ if (words.length === 0) return [];
385
+ const { Tensor } = await import("onnxruntime-web");
386
+ const tokenCount = batch.inputIds[0].length;
387
+ const spanCount = batch.spanIdxs[0].length;
388
+ const big = (values) => BigInt64Array.from(values, BigInt);
389
+ const feeds = {
390
+ input_ids: new Tensor("int64", big(batch.inputIds.flat()), [1, tokenCount]),
391
+ attention_mask: new Tensor("int64", big(batch.attentionMasks.flat()), [1, tokenCount]),
392
+ words_mask: new Tensor("int64", big(batch.wordsMasks.flat()), [1, tokenCount]),
393
+ text_lengths: new Tensor("int64", big(batch.textLengths), [1, 1]),
394
+ span_idx: new Tensor("int64", big(batch.spanIdxs.flat(2)), [
395
+ 1,
396
+ spanCount,
397
+ 2
398
+ ]),
399
+ span_mask: new Tensor("bool", Uint8Array.from(batch.spanMasks.flat(), (v) => v ? 1 : 0), [1, spanCount])
400
+ };
401
+ const inputs = {};
402
+ for (const name of session.inputNames) {
403
+ const tensor = feeds[name];
404
+ if (!tensor) throw new NerError(`Model expects unknown input "${name}"`, "Gliner1Backend");
405
+ inputs[name] = tensor;
406
+ }
407
+ const outputs = await session.run(inputs);
408
+ const logits = (outputs[OUTPUT_LOGITS] ?? outputs[session.outputNames[0] ?? ""])?.data;
409
+ if (!logits) throw new NerError("Model produced no logits", "Gliner1Backend");
410
+ const [spans = []] = decodeSpans(logits, {
411
+ batchSize: 1,
412
+ inputLength: words.length,
413
+ maxWidth: config.maxWidth,
414
+ entityCount: labels.length,
415
+ texts: [text],
416
+ batchWords: batch.batchWords,
417
+ idToClass: batch.idToClass
418
+ }, {
419
+ threshold,
420
+ flatNer: true,
421
+ multiLabel: false
422
+ });
423
+ return spans;
424
+ }
425
+ async dispose() {
426
+ await this.session?.release();
427
+ this.session = null;
428
+ this.processor = null;
429
+ this.config = null;
430
+ }
431
+ };
432
+ //#endregion
433
+ //#region src/ner/vendor/gliner2-decoder.ts
434
+ /** GLiNER2 span generation and decoding. Adapted from @lmoe/gliner-onnx (MIT). */
435
+ /**
436
+ * Every span up to `maxWidth` words.
437
+ *
438
+ * Out-of-range slots are filled with (0, 0) rather than dropped, because the
439
+ * span axis must stay a fixed `seqLen * maxWidth` for the ONNX graph. The
440
+ * decoder skips them by re-checking the word bounds.
441
+ */
442
+ function generateSpans(seqLen, maxWidth) {
443
+ const spanStart = [];
444
+ const spanEnd = [];
445
+ for (let i = 0; i < seqLen; i++) for (let j = 0; j < maxWidth; j++) {
446
+ const inRange = i + j < seqLen;
447
+ spanStart.push(inRange ? i : 0);
448
+ spanEnd.push(inRange ? i + j : 0);
449
+ }
450
+ return {
451
+ spanStart,
452
+ spanEnd,
453
+ spanCount: spanStart.length
454
+ };
455
+ }
456
+ /** Sigmoid of the dot product between each span and each label embedding. */
457
+ function computeDotProductScores(spanRep, labelRep, spanCount, labelCount, hiddenSize) {
458
+ const scores = new Float32Array(spanCount * labelCount);
459
+ for (let s = 0; s < spanCount; s++) {
460
+ const spanOffset = s * hiddenSize;
461
+ for (let l = 0; l < labelCount; l++) {
462
+ const labelOffset = l * hiddenSize;
463
+ let dot = 0;
464
+ for (let h = 0; h < hiddenSize; h++) dot += spanRep[spanOffset + h] * labelRep[labelOffset + h];
465
+ scores[s * labelCount + l] = sigmoid(dot);
466
+ }
467
+ }
468
+ return scores;
469
+ }
470
+ /** Score matrix -> entities, then drop same-label overlaps keeping the best. */
471
+ function decodeEntities(scoreData, wordCount, labels, wordOffsets, text, threshold) {
472
+ const { scores, wordSpanStart, wordSpanEnd, spanCount } = scoreData;
473
+ const labelCount = labels.length;
474
+ const entities = [];
475
+ for (let s = 0; s < spanCount; s++) {
476
+ const startWord = wordSpanStart[s];
477
+ const endWord = wordSpanEnd[s];
478
+ if (startWord >= wordCount || endWord >= wordCount) continue;
479
+ for (let l = 0; l < labelCount; l++) {
480
+ const score = scores[s * labelCount + l];
481
+ if (score < threshold) continue;
482
+ const start = wordOffsets[startWord][0];
483
+ const end = wordOffsets[endWord][1];
484
+ entities.push({
485
+ text: text.slice(start, end),
486
+ label: labels[l],
487
+ start,
488
+ end,
489
+ score
490
+ });
491
+ }
492
+ }
493
+ return deduplicateEntities(entities);
494
+ }
495
+ /**
496
+ * Keep the highest-scoring entity among overlapping ones *of the same label*.
497
+ * Different labels may overlap: a person name inside an organization is a
498
+ * legitimate reading, not a conflict.
499
+ */
500
+ function deduplicateEntities(entities) {
501
+ const byScore = [...entities].sort((a, b) => b.score - a.score);
502
+ const kept = [];
503
+ for (const entity of byScore) if (!kept.some((other) => entity.label === other.label && entity.start < other.end && entity.end > other.start)) kept.push(entity);
504
+ return kept.sort((a, b) => a.start - b.start);
505
+ }
506
+ //#endregion
507
+ //#region src/ner/gliner2-backend.ts
508
+ /**
509
+ * GLiNER2 runtime on onnxruntime-web.
510
+ *
511
+ * Three graphs, orchestrated in JS:
512
+ * encoder input_ids, attention_mask [1, seq] int64 -> hidden states
513
+ * span_rep hidden_states [1, seq, hidden] + span_start_idx/span_end_idx
514
+ * [1, spans] int64 -> span representations
515
+ * count_embed label_embeddings [labels, hidden] -> transformed embeddings
516
+ *
517
+ * The `classifier` graph is for text classification only and is not downloaded.
518
+ * Scoring (dot product + sigmoid) happens in JS between span_rep and
519
+ * count_embed.
520
+ *
521
+ * Unlike GLiNER1's single prompt string, GLiNER2 builds a *schema*:
522
+ * ( [P] entities ( [E] label1 [E] label2 ) ) [SEP_TEXT] <text>
523
+ * with each label's position recorded so its embedding can be gathered from
524
+ * the encoder output.
525
+ */
526
+ const CONFIG_FILE = "config.json";
527
+ const GLINER2_CONFIG_FILE = "gliner2_config.json";
528
+ const TOKEN_P = "[P]";
529
+ const TOKEN_E = "[E]";
530
+ const TOKEN_SEP_TEXT = "[SEP_TEXT]";
531
+ const SCHEMA_OPEN = "(";
532
+ const SCHEMA_CLOSE = ")";
533
+ const NER_TASK_NAME = "entities";
534
+ function decodeJson(buffer, name) {
535
+ if (!buffer) throw new NerError(`Model is missing ${name}`, "Gliner2Backend");
536
+ return JSON.parse(new TextDecoder().decode(buffer));
537
+ }
538
+ var Gliner2Backend = class {
539
+ arch = "gliner2";
540
+ config = null;
541
+ encoder = null;
542
+ spanRep = null;
543
+ countEmbed = null;
544
+ tokenize = null;
545
+ async load(files, options) {
546
+ const transformer = decodeJson(files[CONFIG_FILE], CONFIG_FILE);
547
+ const gliner2 = decodeJson(files[GLINER2_CONFIG_FILE], GLINER2_CONFIG_FILE);
548
+ if (typeof transformer.hidden_size !== "number") throw new NerError(`${CONFIG_FILE} is missing hidden_size`, "Gliner2Backend");
549
+ if (typeof gliner2.max_width !== "number") throw new NerError(`${GLINER2_CONFIG_FILE} is missing max_width`, "Gliner2Backend");
550
+ this.config = {
551
+ hiddenSize: transformer.hidden_size,
552
+ maxWidth: gliner2.max_width,
553
+ specialTokens: gliner2.special_tokens
554
+ };
555
+ const tokenizer = options.tokenizer;
556
+ this.tokenize = (text) => extractTokenIds(tokenizer(text, { add_special_tokens: false }).input_ids.tolist());
557
+ const graph = (path) => {
558
+ const bytes = files[path];
559
+ if (!bytes) throw new NerError(`Model is missing ${path}`, "Gliner2Backend");
560
+ return bytes;
561
+ };
562
+ const providers = options.executionProviders ?? ["wasm"];
563
+ const [encoder, spanRep, countEmbed] = await Promise.all([
564
+ createSession(graph("onnx/encoder.onnx"), { executionProviders: providers }),
565
+ createSession(graph("onnx/span_rep.onnx"), { executionProviders: providers }),
566
+ createSession(graph("onnx/count_embed.onnx"), { executionProviders: providers })
567
+ ]);
568
+ this.encoder = encoder;
569
+ this.spanRep = spanRep;
570
+ this.countEmbed = countEmbed;
571
+ }
572
+ async extract(text, labels, threshold) {
573
+ const { config, tokenize } = this;
574
+ if (!config || !tokenize || !this.encoder || !this.spanRep || !this.countEmbed) throw new NerError("Backend used before load()", "Gliner2Backend");
575
+ if (text.trim().length === 0 || labels.length === 0) return [];
576
+ const schema = this.buildSchema(labels);
577
+ const words = this.tokenizeWords(text);
578
+ if (words.wordOffsets.length === 0) return [];
579
+ const allTokens = [...schema.tokens, ...words.tokens];
580
+ const hidden = await this.encode(allTokens);
581
+ const labelEmbeddings = gatherRows(hidden, schema.labelPositions, config.hiddenSize);
582
+ const textTokenCount = allTokens.length - schema.tokens.length;
583
+ if (textTokenCount === 0) return [];
584
+ const textHidden = sliceRows(hidden, schema.tokens.length, textTokenCount, config.hiddenSize);
585
+ const { spanStart, spanEnd, spanCount } = generateSpans(words.wordOffsets.length, config.maxWidth);
586
+ const toToken = (wordIdx) => words.firstTokenPositions[wordIdx] ?? 0;
587
+ return decodeEntities({
588
+ scores: computeDotProductScores(await this.runSpanRep(textHidden, textTokenCount, spanStart.map(toToken), spanEnd.map(toToken), spanCount), await this.runCountEmbed(labelEmbeddings, labels.length), spanCount, labels.length, config.hiddenSize),
589
+ wordSpanStart: spanStart,
590
+ wordSpanEnd: spanEnd,
591
+ spanCount
592
+ }, words.wordOffsets.length, labels, words.wordOffsets, text, threshold);
593
+ }
594
+ /** `( [P] entities ( [E] label1 [E] label2 ) ) [SEP_TEXT]` */
595
+ buildSchema(labels) {
596
+ const config = this.config;
597
+ const tokenize = this.tokenize;
598
+ const open = tokenize(SCHEMA_OPEN);
599
+ const close = tokenize(SCHEMA_CLOSE);
600
+ const tokens = [...open, config.specialTokens[TOKEN_P]];
601
+ tokens.push(...tokenize(NER_TASK_NAME), ...open);
602
+ const labelPositions = [];
603
+ for (const label of labels) {
604
+ labelPositions.push(tokens.length);
605
+ tokens.push(config.specialTokens[TOKEN_E], ...tokenize(label));
606
+ }
607
+ tokens.push(...close, ...close, config.specialTokens[TOKEN_SEP_TEXT]);
608
+ return {
609
+ tokens,
610
+ labelPositions
611
+ };
612
+ }
613
+ /**
614
+ * Word-split and tokenize the text.
615
+ *
616
+ * Lower-cased before splitting, matching the reference implementation --
617
+ * offsets stay valid because `toLowerCase` is length-preserving for the
618
+ * scripts these models cover.
619
+ */
620
+ tokenizeWords(text) {
621
+ const tokenize = this.tokenize;
622
+ const tokens = [];
623
+ const wordOffsets = [];
624
+ const firstTokenPositions = [];
625
+ for (const [word, start, end] of splitWords(text.toLowerCase(), RICH_WORD_PATTERN)) {
626
+ wordOffsets.push([start, end]);
627
+ firstTokenPositions.push(tokens.length);
628
+ tokens.push(...tokenize(word));
629
+ }
630
+ return {
631
+ tokens,
632
+ wordOffsets,
633
+ firstTokenPositions
634
+ };
635
+ }
636
+ async encode(tokens) {
637
+ const { Tensor } = await import("onnxruntime-web");
638
+ const session = this.encoder;
639
+ const seqLen = tokens.length;
640
+ return firstOutput(await session.run({
641
+ input_ids: new Tensor("int64", BigInt64Array.from(tokens, BigInt), [1, seqLen]),
642
+ attention_mask: new Tensor("int64", new BigInt64Array(seqLen).fill(1n), [1, seqLen])
643
+ }), "encoder");
644
+ }
645
+ async runSpanRep(hidden, seqLen, spanStart, spanEnd, spanCount) {
646
+ const { Tensor } = await import("onnxruntime-web");
647
+ const session = this.spanRep;
648
+ const hiddenSize = this.config.hiddenSize;
649
+ return firstOutput(await session.run({
650
+ hidden_states: new Tensor("float32", hidden, [
651
+ 1,
652
+ seqLen,
653
+ hiddenSize
654
+ ]),
655
+ span_start_idx: new Tensor("int64", BigInt64Array.from(spanStart, BigInt), [1, spanCount]),
656
+ span_end_idx: new Tensor("int64", BigInt64Array.from(spanEnd, BigInt), [1, spanCount])
657
+ }), "span_rep");
658
+ }
659
+ async runCountEmbed(labelEmbeddings, labelCount) {
660
+ const { Tensor } = await import("onnxruntime-web");
661
+ const session = this.countEmbed;
662
+ const hiddenSize = this.config.hiddenSize;
663
+ return firstOutput(await session.run({ label_embeddings: new Tensor("float32", labelEmbeddings, [labelCount, hiddenSize]) }), "count_embed");
664
+ }
665
+ async dispose() {
666
+ await Promise.all([
667
+ this.encoder?.release(),
668
+ this.spanRep?.release(),
669
+ this.countEmbed?.release()
670
+ ]);
671
+ this.encoder = null;
672
+ this.spanRep = null;
673
+ this.countEmbed = null;
674
+ this.config = null;
675
+ this.tokenize = null;
676
+ }
677
+ };
678
+ function firstOutput(outputs, graph) {
679
+ const [name] = Object.keys(outputs);
680
+ if (!name) throw new NerError(`Graph ${graph} produced no output`, "Gliner2Backend");
681
+ return outputs[name].data;
682
+ }
683
+ //#endregion
684
+ //#region src/ner/registry.ts
685
+ /** Generic PII labels, matching the pdftools prototype's default set. */
686
+ const DEFAULT_PII_LABELS = Object.freeze([
687
+ "person",
688
+ "organization",
689
+ "location",
690
+ "email",
691
+ "phone_number",
692
+ "url",
693
+ "id",
694
+ "account_number",
695
+ "zip_code",
696
+ "address",
697
+ "ip_address",
698
+ "date",
699
+ "ssn",
700
+ "driver_license",
701
+ "passport",
702
+ "age",
703
+ "credit_card",
704
+ "medical_condition"
705
+ ]);
706
+ /**
707
+ * Label prompts the StabRise GLiNER2 PII model was fine-tuned on.
708
+ *
709
+ * These must match the cloud endpoint's tag list (scaledp-api
710
+ * deidentify/views.py) or scores drop: GLiNER scores a label by its prompt
711
+ * text, so a renamed label is a different label.
712
+ */
713
+ const GLINER2_PII_LABELS = Object.freeze([
714
+ "date",
715
+ "person_name",
716
+ "person_title",
717
+ "organization",
718
+ "location",
719
+ "email",
720
+ "phone",
721
+ "id",
722
+ "account",
723
+ "zip_code",
724
+ "address",
725
+ "ip",
726
+ "url",
727
+ "ssn",
728
+ "driver_license",
729
+ "passport",
730
+ "age",
731
+ "credit_card",
732
+ "medical_condition",
733
+ "technology"
734
+ ]);
735
+ const NER_MODELS = Object.freeze([
736
+ {
737
+ id: "gliner-multi-pii",
738
+ name: "GLiNER multilingual PII, int8 (~333 MB)",
739
+ arch: "gliner1",
740
+ repo: "onnx-community/gliner_multi_pii-v1",
741
+ files: [{
742
+ path: "gliner_config.json",
743
+ approxBytes: 800
744
+ }, {
745
+ path: "onnx/model_int8.onnx",
746
+ approxBytes: 349e6
747
+ }],
748
+ labels: DEFAULT_PII_LABELS,
749
+ languages: ["multi"]
750
+ },
751
+ {
752
+ id: "gliner-small",
753
+ name: "GLiNER small English, int8 (~183 MB)",
754
+ arch: "gliner1",
755
+ repo: "onnx-community/gliner_small-v2.1",
756
+ files: [{
757
+ path: "gliner_config.json",
758
+ approxBytes: 731
759
+ }, {
760
+ path: "onnx/model_quantized.onnx",
761
+ approxBytes: 183403734
762
+ }],
763
+ labels: DEFAULT_PII_LABELS,
764
+ languages: ["en"]
765
+ },
766
+ {
767
+ id: "stabrise-pii-multi",
768
+ name: "StabRise PII multilingual, int8 (~404 MB)",
769
+ arch: "gliner1",
770
+ repo: "StabRise/pii-detection-en-fr-ge-it-es",
771
+ files: [{
772
+ path: "config.json",
773
+ approxBytes: 3417
774
+ }, {
775
+ path: "model_int8.model",
776
+ approxBytes: 403923207
777
+ }],
778
+ labels: DEFAULT_PII_LABELS,
779
+ languages: [
780
+ "en",
781
+ "fr",
782
+ "de",
783
+ "it",
784
+ "es"
785
+ ],
786
+ private: true
787
+ },
788
+ {
789
+ id: "stabrise-pii-multi-g2",
790
+ name: "StabRise PII multilingual GLiNER2, fp32 (~1.2 GB)",
791
+ arch: "gliner2",
792
+ repo: "StabRise/pii-multi-g2-v1-onnx",
793
+ files: [
794
+ {
795
+ path: "config.json",
796
+ approxBytes: 48
797
+ },
798
+ {
799
+ path: "gliner2_config.json",
800
+ approxBytes: 691
801
+ },
802
+ {
803
+ path: "onnx/encoder.onnx",
804
+ approxBytes: 1111055946
805
+ },
806
+ {
807
+ path: "onnx/span_rep.onnx",
808
+ approxBytes: 66111424
809
+ },
810
+ {
811
+ path: "onnx/count_embed.onnx",
812
+ approxBytes: 42506885
813
+ }
814
+ ],
815
+ labels: GLINER2_PII_LABELS,
816
+ languages: [
817
+ "en",
818
+ "de",
819
+ "pl",
820
+ "es"
821
+ ],
822
+ private: true,
823
+ executionProviders: ["wasm"]
824
+ }
825
+ ]);
826
+ /** Public, zero-configuration default. */
827
+ const DEFAULT_NER_MODEL_ID = "gliner-multi-pii";
828
+ function getNerModel(id) {
829
+ return NER_MODELS.find((m) => m.id === id);
830
+ }
831
+ /** Total download size in bytes, for a progress estimate before fetching. */
832
+ function modelSizeBytes(model) {
833
+ return model.files.reduce((sum, f) => sum + (f.approxBytes ?? 0), 0);
834
+ }
835
+ //#endregion
836
+ //#region src/ner/tokenizer.ts
837
+ /**
838
+ * Tokenizer loading and adaptation for the GLiNER runtimes.
839
+ *
840
+ * `@huggingface/transformers` provides the tokenizer only; no model runs
841
+ * through it. Its remote host is configurable so gated repos can be proxied
842
+ * through the consuming application's own origin.
843
+ */
844
+ let modulePromise = null;
845
+ async function loadTransformers() {
846
+ if (modulePromise) return modulePromise;
847
+ modulePromise = (async () => {
848
+ let mod;
849
+ try {
850
+ mod = await import("@huggingface/transformers");
851
+ } catch (cause) {
852
+ throw new Error("@huggingface/transformers is required for NER tokenization. Install it: npm i @huggingface/transformers", { cause });
853
+ }
854
+ const { hf } = getConfig();
855
+ mod.env.allowLocalModels = false;
856
+ if (hf.remoteHost) mod.env.remoteHost = hf.remoteHost;
857
+ if (hf.remotePathTemplate) mod.env.remotePathTemplate = hf.remotePathTemplate;
858
+ return mod;
859
+ })();
860
+ return modulePromise;
861
+ }
862
+ /** Reset the cached module. Tests only. */
863
+ function resetTransformers() {
864
+ modulePromise = null;
865
+ }
866
+ const tokenizers = /* @__PURE__ */ new Map();
867
+ async function loadTokenizer(repo) {
868
+ const existing = tokenizers.get(repo);
869
+ if (existing) return existing;
870
+ const promise = (async () => {
871
+ const { AutoTokenizer } = await loadTransformers();
872
+ return AutoTokenizer.from_pretrained(repo);
873
+ })();
874
+ promise.catch(() => tokenizers.delete(repo));
875
+ tokenizers.set(repo, promise);
876
+ return promise;
877
+ }
878
+ /** Adapt to the callable form the GLiNER2 runtime expects. */
879
+ function toCallableTokenizer(tokenizer) {
880
+ return (text) => {
881
+ return extractTokenIds(tokenizer(text, { add_special_tokens: false }).input_ids.tolist());
882
+ };
883
+ }
884
+ //#endregion
885
+ //#region src/ner/gliner-ner.ts
886
+ /**
887
+ * Named-entity recognition over a Document, mirroring ScaleDP's `Ner` stage.
888
+ *
889
+ * Chunks long text, runs a GLiNER backend, and maps the resulting character
890
+ * offsets back onto the OCR boxes so every entity carries its position on the
891
+ * page.
892
+ */
893
+ const GLINER_NER_DEFAULTS = Object.freeze({
894
+ ...BASE_STAGE_DEFAULTS,
895
+ inputCol: "text",
896
+ outputCol: "ner",
897
+ keepInputData: true,
898
+ model: DEFAULT_NER_MODEL_ID,
899
+ labels: DEFAULT_PII_LABELS,
900
+ threshold: .5,
901
+ whiteList: [],
902
+ chunkLength: 500,
903
+ chunkStride: 480,
904
+ normaliseCasing: true
905
+ });
906
+ /**
907
+ * Map each character of the joined document text to the box it came from.
908
+ *
909
+ * Built from the *actual* text the OCR stage produced rather than assuming one
910
+ * separator per box. Python derives the mapping from `len(box.text) + 1`, which
911
+ * silently drifts whenever `keepFormatting` inserted several spaces or a
912
+ * newline, shifting every entity's boxes after the first wide gap.
913
+ */
914
+ function buildCharToBoxMap(text, boxes) {
915
+ const mapping = new Int32Array(text.length).fill(-1);
916
+ let cursor = 0;
917
+ for (const [index, box] of boxes.entries()) {
918
+ if (box.text.length === 0) continue;
919
+ const found = text.indexOf(box.text, cursor);
920
+ if (found === -1) continue;
921
+ mapping.fill(index, found, found + box.text.length);
922
+ cursor = found + box.text.length;
923
+ }
924
+ return mapping;
925
+ }
926
+ /** Boxes a character range touches, in document order and without repeats. */
927
+ function boxesForRange(mapping, boxes, start, end) {
928
+ const seen = /* @__PURE__ */ new Set();
929
+ const out = [];
930
+ for (let i = Math.max(0, start); i < Math.min(end, mapping.length); i++) {
931
+ const index = mapping[i];
932
+ if (index < 0 || seen.has(index)) continue;
933
+ seen.add(index);
934
+ const box = boxes[index];
935
+ if (box) out.push(box);
936
+ }
937
+ return out;
938
+ }
939
+ var GlinerNer = class extends Stage {
940
+ name = "GlinerNer";
941
+ backend = null;
942
+ loading = null;
943
+ constructor(options = {}) {
944
+ super(resolveParams(GLINER_NER_DEFAULTS, options, {
945
+ threshold: (value) => {
946
+ if (!(value >= 0 && value <= 1)) throw new RangeError(`threshold must be between 0 and 1, received ${value}`);
947
+ },
948
+ model: (value) => {
949
+ if (!getNerModel(value)) throw new RangeError(`Unknown NER model "${value}". See NER_MODELS for valid ids.`);
950
+ }
951
+ }));
952
+ }
953
+ async init() {
954
+ await this.getBackend();
955
+ }
956
+ getBackend() {
957
+ if (this.backend) return Promise.resolve(this.backend);
958
+ if (this.loading) return this.loading;
959
+ this.loading = (async () => {
960
+ const model = getNerModel(this.params.model);
961
+ if (!model) throw new NerError(`Unknown model ${this.params.model}`, this.name);
962
+ if (model.private && !getConfig().auth) throw new NerError(`Model ${model.id} lives in a private repo. Supply a token via configure({ auth }).`, this.name);
963
+ const [files, tokenizer] = await Promise.all([ensureModelFiles({
964
+ repo: model.repo,
965
+ files: model.files
966
+ }), loadTokenizer(model.repo)]);
967
+ const backend = model.arch === "gliner2" ? new Gliner2Backend() : new Gliner1Backend();
968
+ await backend.load(files, {
969
+ tokenizer,
970
+ executionProviders: model.executionProviders ?? getConfig().executionProviders
971
+ });
972
+ this.backend = backend;
973
+ return backend;
974
+ })();
975
+ this.loading.catch(() => {
976
+ this.loading = null;
977
+ });
978
+ return this.loading;
979
+ }
980
+ async apply(input, row) {
981
+ const document = input;
982
+ if (!document || typeof document.text !== "string") throw new NerError("Expected a Document with text", this.name);
983
+ if (document.exception) throw new NerError(`Upstream stage failed: ${document.exception}`, this.name);
984
+ const entities = await this.extract(document);
985
+ return createNerOutput({
986
+ path: String(row[this.params.pathCol] ?? document.path ?? "memory"),
987
+ entities,
988
+ json: JSON.stringify(entities)
989
+ });
990
+ }
991
+ /** Run NER over a document and attach boxes to every entity found. */
992
+ async extract(document) {
993
+ const { labels, threshold, whiteList, chunkLength, chunkStride } = this.params;
994
+ if (document.text.trim().length === 0 || labels.length === 0) return [];
995
+ const backend = await this.getBackend();
996
+ const source = this.params.normaliseCasing ? normaliseCasing(document.text) : document.text;
997
+ const spans = [];
998
+ for (const chunk of chunkText(source, chunkLength, chunkStride)) {
999
+ const found = await backend.extract(chunk.text, labels, threshold);
1000
+ for (const span of found) spans.push(rebaseSpan(span, chunk.offset));
1001
+ }
1002
+ const mapping = buildCharToBoxMap(document.text, document.bboxes);
1003
+ const allowed = new Set(whiteList);
1004
+ return dedupeSpans(spans).filter((span) => allowed.size === 0 || allowed.has(span.label)).map((span) => ({
1005
+ entity_group: span.label,
1006
+ score: span.score,
1007
+ word: document.text.slice(span.start, span.end),
1008
+ start: span.start,
1009
+ end: span.end,
1010
+ boxes: boxesForRange(mapping, document.bboxes, span.start, span.end)
1011
+ }));
1012
+ }
1013
+ onError(message, row) {
1014
+ return createNerOutput({
1015
+ path: String(row[this.params.pathCol] ?? "memory"),
1016
+ exception: message
1017
+ });
1018
+ }
1019
+ async dispose() {
1020
+ await this.backend?.dispose();
1021
+ this.backend = null;
1022
+ this.loading = null;
1023
+ }
1024
+ };
1025
+ //#endregion
1026
+ export { isMostlyUppercase as A, toSpanTokenizer as C, DEFAULT_CHUNK_STRIDE as D, DEFAULT_CHUNK_LENGTH as E, rebaseSpan as M, titleCaseAllCapsWords as N, chunkText as O, decodeSpans as S, softmax as T, generateSpans as _, loadTokenizer as a, WORD_PATTERN as b, DEFAULT_NER_MODEL_ID as c, NER_MODELS as d, getNerModel as f, decodeEntities as g, computeDotProductScores as h, buildCharToBoxMap as i, normaliseCasing as j, dedupeSpans as k, DEFAULT_PII_LABELS as l, Gliner2Backend as m, GlinerNer as n, resetTransformers as o, modelSizeBytes as p, boxesForRange as r, toCallableTokenizer as s, GLINER_NER_DEFAULTS as t, GLINER2_PII_LABELS as u, Gliner1Backend as v, sigmoid as w, splitWords as x, RICH_WORD_PATTERN as y };
1027
+
1028
+ //# sourceMappingURL=ner-SsZLZ6ed.js.map