@doccloak/core 0.11.0 → 0.12.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 +121 -2
- package/README.md +252 -19
- package/dist/doc.d.ts +68 -1
- package/dist/doc.js +447 -62
- package/dist/dom/docx.d.ts +135 -7
- package/dist/dom/docx.js +752 -125
- package/dist/dom/drawingml.d.ts +59 -0
- package/dist/dom/drawingml.js +176 -0
- package/dist/dom/errors.d.ts +35 -0
- package/dist/dom/errors.js +24 -0
- package/dist/dom/index.d.ts +3 -0
- package/dist/dom/index.js +6 -0
- package/dist/dom/office.d.ts +56 -0
- package/dist/dom/office.js +76 -23
- package/dist/dom/opc.d.ts +135 -7
- package/dist/dom/opc.js +407 -14
- package/dist/dom/package-policy.d.ts +137 -0
- package/dist/dom/package-policy.js +355 -0
- package/dist/dom/xlsx.d.ts +64 -6
- package/dist/dom/xlsx.js +75 -20
- package/dist/engine.d.ts +18 -0
- package/dist/engine.js +37 -13
- package/dist/env.d.ts +21 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +10 -5
- package/dist/model-loader.d.ts +61 -6
- package/dist/model-loader.js +122 -17
- package/dist/pipeline.d.ts +87 -8
- package/dist/pipeline.js +249 -50
- package/dist/providers/bardsai.d.ts +8 -1
- package/dist/providers/bardsai.js +22 -2
- package/dist/providers/gliner-base.d.ts +8 -1
- package/dist/providers/gliner-base.js +22 -2
- package/dist/providers/gliner.d.ts +8 -1
- package/dist/providers/gliner.js +22 -2
- package/dist/regex/loader.d.ts +2 -0
- package/dist/regex/loader.js +2 -0
- package/dist/regex/rules.data.js +15 -14
- package/dist/regex/types.d.ts +6 -0
- package/dist/session.d.ts +101 -3
- package/dist/session.js +397 -43
- package/dist/surrogates.js +57 -45
- package/dist/worker-protocol.d.ts +13 -1
- package/dist/worker-protocol.js +164 -12
- package/package.json +1 -1
- package/rules/at.json +1 -1
- package/rules/be.json +1 -1
- package/rules/ch.json +1 -1
- package/rules/de.json +1 -1
- package/rules/dk.json +1 -1
- package/rules/es.json +1 -1
- package/rules/fi.json +1 -1
- package/rules/fr.json +1 -1
- package/rules/it.json +1 -1
- package/rules/no.json +1 -1
- package/rules/pl.json +1 -1
- package/rules/pt.json +1 -1
- package/rules/schema.json +4 -0
- package/rules/se.json +1 -1
- package/rules/universal.json +2 -1
package/dist/pipeline.js
CHANGED
|
@@ -1,40 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure entity pipeline (T005).
|
|
3
3
|
*
|
|
4
|
-
* Extracted
|
|
5
|
-
* entities
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Extracted from the web app's detection.worker.ts: text and entities in,
|
|
5
|
+
* entities out. No worker messaging, no providers, no environment access.
|
|
6
|
+
* The worker (and any other host) composes these functions around its own
|
|
7
|
+
* ML provider call.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* of
|
|
9
|
+
* Security pass 2026-09:
|
|
10
|
+
* - T180: detectWithRegex runs every rule line by line (rules flagged
|
|
11
|
+
* `multiline` see the whole text), so quadratic patterns cost O(line^2)
|
|
12
|
+
* instead of O(document^2).
|
|
13
|
+
* - T181: filterFalsePositives is script-aware (CJK/Thai and PERSON values
|
|
14
|
+
* of two characters survive) and propagateEntities uses Unicode letter/
|
|
15
|
+
* digit boundaries, exact offsets on the original text and a common-word
|
|
16
|
+
* guard for lowercase PERSON matches.
|
|
13
17
|
*/
|
|
14
18
|
import { ALL_REGEX_RULES } from "./regex/index.js";
|
|
19
|
+
/**
|
|
20
|
+
* Split on '\n' keeping offsets. '\r' stays inside the line (a CRLF line then
|
|
21
|
+
* ends with '\r', exactly as it did when the whole text was matched at once).
|
|
22
|
+
* Empty lines are skipped: no rule can match the empty string.
|
|
23
|
+
*/
|
|
24
|
+
export function segmentLines(text) {
|
|
25
|
+
const segments = [];
|
|
26
|
+
let lineStart = 0;
|
|
27
|
+
while (lineStart <= text.length) {
|
|
28
|
+
const newline = text.indexOf('\n', lineStart);
|
|
29
|
+
const lineEnd = newline === -1 ? text.length : newline;
|
|
30
|
+
if (lineEnd > lineStart) {
|
|
31
|
+
segments.push({ text: text.slice(lineStart, lineEnd), offset: lineStart });
|
|
32
|
+
}
|
|
33
|
+
if (newline === -1)
|
|
34
|
+
break;
|
|
35
|
+
lineStart = newline + 1;
|
|
36
|
+
}
|
|
37
|
+
return segments;
|
|
38
|
+
}
|
|
39
|
+
function runRule(rule, segment, out) {
|
|
40
|
+
rule.pattern.lastIndex = 0;
|
|
41
|
+
let match;
|
|
42
|
+
while ((match = rule.pattern.exec(segment.text)) !== null) {
|
|
43
|
+
if (match[0].length === 0) {
|
|
44
|
+
// Defensive: a zero-width match would otherwise loop forever.
|
|
45
|
+
rule.pattern.lastIndex++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (rule.validate && !rule.validate(match[0]))
|
|
49
|
+
continue;
|
|
50
|
+
out.push({
|
|
51
|
+
type: rule.type,
|
|
52
|
+
value: match[0],
|
|
53
|
+
start: segment.offset + match.index,
|
|
54
|
+
end: segment.offset + match.index + match[0].length,
|
|
55
|
+
confidence: rule.confidence,
|
|
56
|
+
detector: rule.detector,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Run the regex rules of a region over the text.
|
|
62
|
+
*
|
|
63
|
+
* Rules without `multiline` run on each line separately (offsets are added
|
|
64
|
+
* back, so spans are document-absolute); rules with `multiline: true` run on
|
|
65
|
+
* the whole text. Output order is rule-major, then position, as before.
|
|
66
|
+
*
|
|
67
|
+
* Complexity note (T180): several street/company rules are quadratic in the
|
|
68
|
+
* length of their input. Per-line matching bounds that to the longest line,
|
|
69
|
+
* so 100 KB of ordinary prose is fast; a single 100 KB line without newlines
|
|
70
|
+
* is still slow until the patterns themselves are bounded (T198).
|
|
71
|
+
*/
|
|
15
72
|
export function detectWithRegex(text, regexRegion = 'all') {
|
|
16
73
|
const entities = [];
|
|
17
74
|
const rules = regexRegion === 'all'
|
|
18
75
|
? ALL_REGEX_RULES
|
|
19
76
|
: ALL_REGEX_RULES.filter((r) => r.region === 'universal' || r.region === regexRegion);
|
|
77
|
+
const whole = { text, offset: 0 };
|
|
78
|
+
const lines = segmentLines(text);
|
|
20
79
|
for (const rule of rules) {
|
|
21
|
-
rule.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
type: rule.type,
|
|
28
|
-
value: match[0],
|
|
29
|
-
start: match.index,
|
|
30
|
-
end: match.index + match[0].length,
|
|
31
|
-
confidence: rule.confidence,
|
|
32
|
-
detector: rule.detector,
|
|
33
|
-
});
|
|
80
|
+
if (rule.multiline) {
|
|
81
|
+
runRule(rule, whole, entities);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
for (const line of lines)
|
|
85
|
+
runRule(rule, line, entities);
|
|
34
86
|
}
|
|
35
87
|
}
|
|
36
88
|
return entities;
|
|
37
89
|
}
|
|
90
|
+
// ── resolveOverlaps ────────────────────────────────────────
|
|
38
91
|
export function resolveOverlaps(entities) {
|
|
39
92
|
const sorted = [...entities].sort((a, b) => {
|
|
40
93
|
if (a.start !== b.start)
|
|
@@ -54,13 +107,152 @@ export function resolveOverlaps(entities) {
|
|
|
54
107
|
}
|
|
55
108
|
return resolved;
|
|
56
109
|
}
|
|
110
|
+
// ── trimSpanPunctuation ────────────────────────────────────
|
|
111
|
+
const LEADING_PUNCT = /^[\s(\[{"'«“‘„]+/u;
|
|
112
|
+
const TRAILING_PUNCT = /[\s,.;:!?)\]}"'»”’]+$/u;
|
|
113
|
+
/**
|
|
114
|
+
* Word-level providers (BardS.ai splits on whitespace) return spans that
|
|
115
|
+
* carry the punctuation glued to the last word: "Anna Nowak," or
|
|
116
|
+
* "jan@example.com.". The redacted text then loses that punctuation and the
|
|
117
|
+
* mapping stores it as part of the value. Trim a fixed set of opening and
|
|
118
|
+
* closing punctuation at both ends of every span (T204); a span that would
|
|
119
|
+
* become empty is dropped. Regex spans are not passed through this: their
|
|
120
|
+
* patterns already stop at the identifier.
|
|
121
|
+
*/
|
|
122
|
+
export function trimSpanPunctuation(entities) {
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const entity of entities) {
|
|
125
|
+
const value = entity.value;
|
|
126
|
+
const lead = LEADING_PUNCT.exec(value)?.[0].length ?? 0;
|
|
127
|
+
const trail = TRAILING_PUNCT.exec(value.slice(lead))?.[0].length ?? 0;
|
|
128
|
+
if (lead === 0 && trail === 0) {
|
|
129
|
+
out.push(entity);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const trimmed = value.slice(lead, value.length - trail);
|
|
133
|
+
if (trimmed.length === 0)
|
|
134
|
+
continue;
|
|
135
|
+
out.push({ ...entity, value: trimmed, start: entity.start + lead, end: entity.end - trail });
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
// ── filterFalsePositives ───────────────────────────────────
|
|
140
|
+
/**
|
|
141
|
+
* Scripts whose names are routinely one or two characters long (and whose
|
|
142
|
+
* words are not space-delimited). A value containing any of these keeps the
|
|
143
|
+
* lower two-character threshold for every entity type.
|
|
144
|
+
*/
|
|
145
|
+
const CJK_THAI = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Thai}]/u;
|
|
146
|
+
/**
|
|
147
|
+
* Two-character PERSON values that are titles, suffixes or abbreviations,
|
|
148
|
+
* never names. Compared against the lowercased trimmed value (T181, R2).
|
|
149
|
+
*/
|
|
150
|
+
export const PERSON_SHORT_STOPLIST = new Set([
|
|
151
|
+
'mr', 'ms', 'dr', 'jr', 'sr', 'st', 'ii', 'iv', 'vi', 'no', 're', 'cc',
|
|
152
|
+
]);
|
|
153
|
+
function codePointLength(value) {
|
|
154
|
+
let n = 0;
|
|
155
|
+
for (const _ of value)
|
|
156
|
+
n++;
|
|
157
|
+
return n;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Drop ML spans that are too short to be real entities (R2):
|
|
161
|
+
* - values containing CJK or Thai characters: at least 2 code points, any type
|
|
162
|
+
* - PERSON: at least 2 characters, unless the value is a known 2-letter
|
|
163
|
+
* title/suffix (PERSON_SHORT_STOPLIST); initials like "J." pass
|
|
164
|
+
* - every other type: at least 3 characters (unchanged)
|
|
165
|
+
*/
|
|
57
166
|
export function filterFalsePositives(entities) {
|
|
58
167
|
return entities.filter((e) => {
|
|
59
|
-
|
|
168
|
+
const value = e.value.trim();
|
|
169
|
+
if (value.length === 0)
|
|
60
170
|
return false;
|
|
61
|
-
|
|
171
|
+
if (CJK_THAI.test(value))
|
|
172
|
+
return codePointLength(value) >= 2;
|
|
173
|
+
if (e.type === 'PERSON') {
|
|
174
|
+
if (value.length < 2)
|
|
175
|
+
return false;
|
|
176
|
+
return !PERSON_SHORT_STOPLIST.has(value.toLowerCase());
|
|
177
|
+
}
|
|
178
|
+
return value.length >= 3;
|
|
62
179
|
});
|
|
63
180
|
}
|
|
181
|
+
// ── propagateEntities ──────────────────────────────────────
|
|
182
|
+
/**
|
|
183
|
+
* Words that are also given names or surnames, per language (T181, R15).
|
|
184
|
+
* A PERSON term found in the text with DIFFERENT casing than the detected
|
|
185
|
+
* entity, and NOT starting with an uppercase letter, is skipped when its
|
|
186
|
+
* lowercase form is listed here: "Bill" the person never turns "the bill is
|
|
187
|
+
* due" into "[PERSON_1] is due". Capitalised and ALL-CAPS mentions always
|
|
188
|
+
* propagate, and lowercase mentions of ordinary names ("thanks john") are
|
|
189
|
+
* still caught (golden prop-03-case-insensitive).
|
|
190
|
+
*
|
|
191
|
+
* Only words that appear in lowercase in running prose belong here. German
|
|
192
|
+
* nouns are always capitalised in prose, so the DE list holds adjectives and
|
|
193
|
+
* month names that double as surnames or first names; the PL list holds
|
|
194
|
+
* nouns (animals, months, plants) that are frequent Polish surnames and
|
|
195
|
+
* first names.
|
|
196
|
+
*/
|
|
197
|
+
export const COMMON_WORD_NAMES = {
|
|
198
|
+
en: [
|
|
199
|
+
'bill', 'mark', 'will', 'may', 'art', 'grace', 'hope', 'rose', 'jack', 'sue',
|
|
200
|
+
'pat', 'chase', 'dawn', 'june', 'carol', 'guy', 'rich', 'frank', 'ray', 'don',
|
|
201
|
+
'bob', 'jay', 'gene',
|
|
202
|
+
],
|
|
203
|
+
pl: [
|
|
204
|
+
'lis', 'wilk', 'wrona', 'sowa', 'kruk', 'baran', 'kot', 'mucha', 'kowal', 'sroka',
|
|
205
|
+
'wróbel', 'dudek', 'mróz', 'zając', 'sobota', 'piątek', 'maj', 'marzec', 'kwiecień',
|
|
206
|
+
'lipiec', 'róża', 'jagoda', 'malina', 'kalina', 'cebula',
|
|
207
|
+
],
|
|
208
|
+
de: [
|
|
209
|
+
'lang', 'klein', 'groß', 'gross', 'weiß', 'weiss', 'schwarz', 'braun', 'roth', 'kurz',
|
|
210
|
+
'jung', 'alt', 'neu', 'ernst', 'reich', 'stark', 'wild', 'frei', 'gut', 'voll',
|
|
211
|
+
'kühn', 'fromm', 'hell', 'klug', 'treu', 'lieb', 'mai',
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
const COMMON_WORD_NAME_SET = new Set(Object.values(COMMON_WORD_NAMES).flat());
|
|
215
|
+
/** A character that continues a word: a Unicode letter or digit. */
|
|
216
|
+
const WORD_CHAR = /[\p{L}\p{N}]/u;
|
|
217
|
+
/**
|
|
218
|
+
* Scripts written without spaces between words. A term edge in one of these
|
|
219
|
+
* scripts cannot demand a non-letter neighbour (a Chinese name is normally
|
|
220
|
+
* glued to the next character), mirroring the restore-side rule that CJK
|
|
221
|
+
* keys carry no boundaries.
|
|
222
|
+
*/
|
|
223
|
+
const UNSPACED_SCRIPT = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Thai}]$/u;
|
|
224
|
+
function escapeRegExp(value) {
|
|
225
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
226
|
+
}
|
|
227
|
+
function codePointBefore(text, index) {
|
|
228
|
+
if (index <= 0)
|
|
229
|
+
return '';
|
|
230
|
+
const pair = Array.from(text.slice(Math.max(0, index - 2), index));
|
|
231
|
+
return pair[pair.length - 1] ?? '';
|
|
232
|
+
}
|
|
233
|
+
function codePointAt(text, index) {
|
|
234
|
+
if (index >= text.length)
|
|
235
|
+
return '';
|
|
236
|
+
const cp = text.codePointAt(index);
|
|
237
|
+
return cp === undefined ? '' : String.fromCodePoint(cp);
|
|
238
|
+
}
|
|
239
|
+
function startsUppercase(value) {
|
|
240
|
+
const first = codePointAt(value, 0);
|
|
241
|
+
return first !== '' && first !== first.toLowerCase();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Find every further occurrence of each detected value (and of the words of
|
|
245
|
+
* multi-word values) in the text and emit it as a 'propagated' entity.
|
|
246
|
+
*
|
|
247
|
+
* Terms: whole values of at least 2 characters; words of multi-word values
|
|
248
|
+
* of at least 4 characters (trailing punctuation stripped). Terms of at most
|
|
249
|
+
* 2 characters match with exact case; longer terms match case-insensitively
|
|
250
|
+
* (Unicode case folding via the 'iu' flags) on the ORIGINAL text, so offsets
|
|
251
|
+
* are exact even after characters whose lowercase form has a different
|
|
252
|
+
* length (Turkish dotted İ). A hit needs a non-letter, non-digit neighbour
|
|
253
|
+
* on both sides ([\p{L}\p{N}] complement), except on a side where the term
|
|
254
|
+
* itself ends in an unspaced script (CJK, Thai).
|
|
255
|
+
*/
|
|
64
256
|
export function propagateEntities(text, entities) {
|
|
65
257
|
const propagated = [];
|
|
66
258
|
const seen = new Set();
|
|
@@ -71,7 +263,7 @@ export function propagateEntities(text, entities) {
|
|
|
71
263
|
const addedTerms = new Set();
|
|
72
264
|
for (const e of entities) {
|
|
73
265
|
const val = e.value.trim();
|
|
74
|
-
if (val
|
|
266
|
+
if (codePointLength(val) >= 2 && !addedTerms.has(val.toLowerCase())) {
|
|
75
267
|
addedTerms.add(val.toLowerCase());
|
|
76
268
|
terms.push({ term: val, type: e.type });
|
|
77
269
|
}
|
|
@@ -86,41 +278,48 @@ export function propagateEntities(text, entities) {
|
|
|
86
278
|
}
|
|
87
279
|
}
|
|
88
280
|
for (const { term, type } of terms) {
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
281
|
+
const exactCase = codePointLength(term) <= 2;
|
|
282
|
+
const pattern = new RegExp(escapeRegExp(term), exactCase ? 'gu' : 'giu');
|
|
283
|
+
const needsBoundaryBefore = !UNSPACED_SCRIPT.test(codePointAt(term, 0));
|
|
284
|
+
const needsBoundaryAfter = !UNSPACED_SCRIPT.test(codePointBefore(term, term.length));
|
|
285
|
+
let match;
|
|
286
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
287
|
+
const idx = match.index;
|
|
288
|
+
const found = match[0];
|
|
289
|
+
const end = idx + found.length;
|
|
290
|
+
// Same overlap semantics as the original indexOf loop (searchFrom = idx + 1).
|
|
291
|
+
pattern.lastIndex = idx + 1;
|
|
97
292
|
const key = `${idx}:${end}`;
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
end,
|
|
110
|
-
confidence: 0.95,
|
|
111
|
-
detector: 'propagated',
|
|
112
|
-
});
|
|
113
|
-
}
|
|
293
|
+
if (seen.has(key))
|
|
294
|
+
continue;
|
|
295
|
+
if (needsBoundaryBefore && WORD_CHAR.test(codePointBefore(text, idx)))
|
|
296
|
+
continue;
|
|
297
|
+
if (needsBoundaryAfter && WORD_CHAR.test(codePointAt(text, end)))
|
|
298
|
+
continue;
|
|
299
|
+
if (type === 'PERSON'
|
|
300
|
+
&& found !== term
|
|
301
|
+
&& !startsUppercase(found)
|
|
302
|
+
&& COMMON_WORD_NAME_SET.has(found.toLowerCase())) {
|
|
303
|
+
continue;
|
|
114
304
|
}
|
|
115
|
-
|
|
305
|
+
seen.add(key);
|
|
306
|
+
propagated.push({
|
|
307
|
+
type,
|
|
308
|
+
value: found,
|
|
309
|
+
start: idx,
|
|
310
|
+
end,
|
|
311
|
+
confidence: 0.95,
|
|
312
|
+
detector: 'propagated',
|
|
313
|
+
});
|
|
116
314
|
}
|
|
117
315
|
}
|
|
118
316
|
return propagated;
|
|
119
317
|
}
|
|
318
|
+
// ── detectEntities ─────────────────────────────────────────
|
|
120
319
|
export function detectEntities(text, mlEntities, regexEntities) {
|
|
121
320
|
if (!text.trim())
|
|
122
321
|
return [];
|
|
123
|
-
const mlResults = filterFalsePositives(mlEntities);
|
|
322
|
+
const mlResults = filterFalsePositives(trimSpanPunctuation(mlEntities));
|
|
124
323
|
const regexResults = regexEntities;
|
|
125
324
|
const initial = resolveOverlaps([...mlResults, ...regexResults]);
|
|
126
325
|
const propagated = propagateEntities(text, initial);
|
|
@@ -12,11 +12,18 @@
|
|
|
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';
|
|
15
|
+
import type { ModelVerification, TokenizerFiles } from '../model-loader.js';
|
|
16
16
|
export declare const BARDSAI_MODEL_REVISION = "0e72e19f030ed4e661b1673e549af8e0dd176386";
|
|
17
17
|
/** SHA-256 of onnx/model_quantized.onnx (278,736,360 bytes) at BARDSAI_MODEL_REVISION. */
|
|
18
18
|
export declare const BARDSAI_MODEL_SHA256 = "8c9f555c743ed14eb7e505ff9d9c7785775a1671fd14031af33f424de8bd0e7b";
|
|
19
19
|
export declare const BARDSAI_MODEL_URL = "https://huggingface.co/bardsai/eu-pii-anonimization-multilang/resolve/0e72e19f030ed4e661b1673e549af8e0dd176386/onnx/model_quantized.onnx";
|
|
20
|
+
/**
|
|
21
|
+
* Tokenizer files at the same pinned commit (T185): fetched through
|
|
22
|
+
* fetchModelBlob with SHA-256 + size, cached next to the model and handed to
|
|
23
|
+
* CoreEnv.buildTokenizer. Hashes re-measured against the resolve/<commit>
|
|
24
|
+
* URLs on 2026-09-24 (documentation/model-provenance.md).
|
|
25
|
+
*/
|
|
26
|
+
export declare const BARDSAI_TOKENIZER_FILES: TokenizerFiles;
|
|
20
27
|
export declare class BardsaiProvider implements DetectionProvider {
|
|
21
28
|
private _name;
|
|
22
29
|
get name(): string;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// wasm fallback, asyncify binary). The bare entry resolves the deprecated JSEP
|
|
15
15
|
// bundle whose wasm exceeds Cloudflare Pages' 25 MiB per-file limit.
|
|
16
16
|
import * as ort from 'onnxruntime-web/webgpu';
|
|
17
|
-
import { evictModelFromCache, fetchModelBlob,
|
|
17
|
+
import { evictModelFromCache, fetchModelBlob, loadPinnedTokenizer } from "../model-loader.js";
|
|
18
18
|
// ── Model config ──────────────────────────────────────────
|
|
19
19
|
// Supply-chain pinning (T116): the model is fetched from an immutable
|
|
20
20
|
// commit revision, never from mutable resolve/main, and the downloaded
|
|
@@ -31,6 +31,26 @@ export const BARDSAI_MODEL_URL = `https://huggingface.co/bardsai/eu-pii-anonimiz
|
|
|
31
31
|
const MODEL_URL = BARDSAI_MODEL_URL;
|
|
32
32
|
/** Pre-pinning download URL; its cache entry is evicted best-effort on load. */
|
|
33
33
|
const LEGACY_MODEL_URL = 'https://huggingface.co/bardsai/eu-pii-anonimization-multilang/resolve/main/onnx/model_quantized.onnx';
|
|
34
|
+
/**
|
|
35
|
+
* Tokenizer files at the same pinned commit (T185): fetched through
|
|
36
|
+
* fetchModelBlob with SHA-256 + size, cached next to the model and handed to
|
|
37
|
+
* CoreEnv.buildTokenizer. Hashes re-measured against the resolve/<commit>
|
|
38
|
+
* URLs on 2026-09-24 (documentation/model-provenance.md).
|
|
39
|
+
*/
|
|
40
|
+
export const BARDSAI_TOKENIZER_FILES = [
|
|
41
|
+
{
|
|
42
|
+
url: `https://huggingface.co/bardsai/eu-pii-anonimization-multilang/resolve/${BARDSAI_MODEL_REVISION}/tokenizer.json`,
|
|
43
|
+
sha256: '2464f9721707cb3d5edcf9a3d73454b13e8a7b3bb8fdba94b3de3d843f30e946',
|
|
44
|
+
size: 16781584,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
url: `https://huggingface.co/bardsai/eu-pii-anonimization-multilang/resolve/${BARDSAI_MODEL_REVISION}/tokenizer_config.json`,
|
|
48
|
+
sha256: 'c019e3e4f7f901adf680dde303c7d964de86675a13e089bcc0f614d3fce75333',
|
|
49
|
+
size: 314,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
const TOKENIZER_FILES = BARDSAI_TOKENIZER_FILES;
|
|
53
|
+
/** Hugging Face id for the deprecated CoreEnv.loadTokenizer fallback only. */
|
|
34
54
|
const TOKENIZER_HF = 'bardsai/eu-pii-anonimization-multilang';
|
|
35
55
|
const MODEL_NAME = 'BardS.ai EU PII';
|
|
36
56
|
const DEFAULT_THRESHOLD = 0.5;
|
|
@@ -181,7 +201,7 @@ export class BardsaiProvider {
|
|
|
181
201
|
}),
|
|
182
202
|
];
|
|
183
203
|
if (!this.tokenizer) {
|
|
184
|
-
tasks.push(
|
|
204
|
+
tasks.push(loadPinnedTokenizer(this.env, TOKENIZER_FILES, TOKENIZER_HF).then((t) => {
|
|
185
205
|
this.tokenizer = t;
|
|
186
206
|
}));
|
|
187
207
|
}
|
|
@@ -17,12 +17,19 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { DetectedEntity, DetectionProvider, ProgressCallback } from '../types.js';
|
|
19
19
|
import type { CoreEnv } from '../env.js';
|
|
20
|
-
import type { ModelVerification } from '../model-loader.js';
|
|
20
|
+
import type { ModelVerification, TokenizerFiles } from '../model-loader.js';
|
|
21
21
|
import { type RawSpan } from './gliner.js';
|
|
22
22
|
export declare const GLINER_BASE_MODEL_REVISION = "61726e0ad791dcab3e29339bbec3ad42ded65641";
|
|
23
23
|
/** SHA-256 of onnx/model_quint8.onnx (196,757,174 bytes) at GLINER_BASE_MODEL_REVISION. */
|
|
24
24
|
export declare const GLINER_BASE_MODEL_SHA256 = "0514c8fd86d0513ce5351a3267f132b57d5bcd8f99a90d43cde1228092881d19";
|
|
25
25
|
export declare const GLINER_BASE_MODEL_URL = "https://huggingface.co/knowledgator/gliner-pii-base-v1.0/resolve/61726e0ad791dcab3e29339bbec3ad42ded65641/onnx/model_quint8.onnx";
|
|
26
|
+
/**
|
|
27
|
+
* Tokenizer files at the same pinned commit (T185): fetched through
|
|
28
|
+
* fetchModelBlob with SHA-256 + size, cached next to the model and handed to
|
|
29
|
+
* CoreEnv.buildTokenizer. Hashes re-measured against the resolve/<commit>
|
|
30
|
+
* URLs on 2026-09-24 (documentation/model-provenance.md).
|
|
31
|
+
*/
|
|
32
|
+
export declare const GLINER_BASE_TOKENIZER_FILES: TokenizerFiles;
|
|
26
33
|
/** max_width from the repo's gliner_config.json: spans cover 1..12 words. */
|
|
27
34
|
export declare const GLINER_BASE_MAX_WIDTH = 12;
|
|
28
35
|
export interface SpanIndices {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// wasm fallback, asyncify binary). The bare entry resolves the deprecated JSEP
|
|
20
20
|
// bundle whose wasm exceeds Cloudflare Pages' 25 MiB per-file limit.
|
|
21
21
|
import * as ort from 'onnxruntime-web/webgpu';
|
|
22
|
-
import { fetchModelBlob,
|
|
22
|
+
import { fetchModelBlob, loadPinnedTokenizer } from "../model-loader.js";
|
|
23
23
|
import { DEFAULT_PII_LABELS, greedySelect, mapLabelToEntityType, sigmoid, splitWords, } from "./gliner.js";
|
|
24
24
|
// ── Model config ──────────────────────────────────────────
|
|
25
25
|
// Supply-chain pinning (T116): the model is fetched from an immutable
|
|
@@ -35,6 +35,26 @@ export const GLINER_BASE_MODEL_REVISION = '61726e0ad791dcab3e29339bbec3ad42ded65
|
|
|
35
35
|
export const GLINER_BASE_MODEL_SHA256 = '0514c8fd86d0513ce5351a3267f132b57d5bcd8f99a90d43cde1228092881d19';
|
|
36
36
|
export const GLINER_BASE_MODEL_URL = `https://huggingface.co/knowledgator/gliner-pii-base-v1.0/resolve/${GLINER_BASE_MODEL_REVISION}/onnx/model_quint8.onnx`;
|
|
37
37
|
const DEFAULT_MODEL_URL = GLINER_BASE_MODEL_URL;
|
|
38
|
+
/**
|
|
39
|
+
* Tokenizer files at the same pinned commit (T185): fetched through
|
|
40
|
+
* fetchModelBlob with SHA-256 + size, cached next to the model and handed to
|
|
41
|
+
* CoreEnv.buildTokenizer. Hashes re-measured against the resolve/<commit>
|
|
42
|
+
* URLs on 2026-09-24 (documentation/model-provenance.md).
|
|
43
|
+
*/
|
|
44
|
+
export const GLINER_BASE_TOKENIZER_FILES = [
|
|
45
|
+
{
|
|
46
|
+
url: `https://huggingface.co/knowledgator/gliner-pii-base-v1.0/resolve/${GLINER_BASE_MODEL_REVISION}/tokenizer.json`,
|
|
47
|
+
sha256: 'ee028763434d18611c1c36356ea1d050e90a9fa94ede57fac48b39f85f818ad1',
|
|
48
|
+
size: 8649232,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
url: `https://huggingface.co/knowledgator/gliner-pii-base-v1.0/resolve/${GLINER_BASE_MODEL_REVISION}/tokenizer_config.json`,
|
|
52
|
+
sha256: '3ec8a90d8758fbc56d50831990c3a3a65660f020c5b06534adf43b04091ffa9e',
|
|
53
|
+
size: 1691,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
const TOKENIZER_FILES = GLINER_BASE_TOKENIZER_FILES;
|
|
57
|
+
/** Hugging Face id for the deprecated CoreEnv.loadTokenizer fallback only. */
|
|
38
58
|
const DEFAULT_TOKENIZER_HF = 'knowledgator/gliner-pii-base-v1.0';
|
|
39
59
|
const DEFAULT_MODEL_NAME = 'GLiNER PII Base';
|
|
40
60
|
const CUSTOM_LABELS_STORAGE_KEY = 'doccloak-custom-labels';
|
|
@@ -208,7 +228,7 @@ export class GlinerBaseProvider {
|
|
|
208
228
|
}),
|
|
209
229
|
];
|
|
210
230
|
if (!this.tokenizer) {
|
|
211
|
-
tasks.push(
|
|
231
|
+
tasks.push(loadPinnedTokenizer(this.env, TOKENIZER_FILES, DEFAULT_TOKENIZER_HF).then((t) => {
|
|
212
232
|
this.tokenizer = t;
|
|
213
233
|
}));
|
|
214
234
|
}
|
|
@@ -8,11 +8,18 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { DetectedEntity, EntityType, DetectionProvider, ProgressCallback } from '../types.js';
|
|
10
10
|
import type { CoreEnv } from '../env.js';
|
|
11
|
-
import type { ModelVerification } from '../model-loader.js';
|
|
11
|
+
import type { ModelVerification, TokenizerFiles } from '../model-loader.js';
|
|
12
12
|
export declare const GLINER_MODEL_REVISION = "d21aad5b4a7ec82b3d0970fd1ac74a12c087d85e";
|
|
13
13
|
/** SHA-256 of onnx/model_quint8.onnx (82,680,500 bytes) at GLINER_MODEL_REVISION. */
|
|
14
14
|
export declare const GLINER_MODEL_SHA256 = "891589426ee96f2748b16439f44fad8c3f97e198e002a6637e58dee989500216";
|
|
15
15
|
export declare const GLINER_MODEL_URL = "https://huggingface.co/knowledgator/gliner-pii-small-v1.0/resolve/d21aad5b4a7ec82b3d0970fd1ac74a12c087d85e/onnx/model_quint8.onnx";
|
|
16
|
+
/**
|
|
17
|
+
* Tokenizer files at the same pinned commit (T185): fetched through
|
|
18
|
+
* fetchModelBlob with SHA-256 + size, cached next to the model and handed to
|
|
19
|
+
* CoreEnv.buildTokenizer. Hashes re-measured against the resolve/<commit>
|
|
20
|
+
* URLs on 2026-09-24 (documentation/model-provenance.md).
|
|
21
|
+
*/
|
|
22
|
+
export declare const GLINER_TOKENIZER_FILES: TokenizerFiles;
|
|
16
23
|
/**
|
|
17
24
|
* Built-in zero-shot label set, shared with the GLiNER PII Base provider
|
|
18
25
|
* (gliner-base.ts) - the models are from the same zero-shot family, so the
|
package/dist/providers/gliner.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// wasm fallback, asyncify binary). The bare entry resolves the deprecated JSEP
|
|
11
11
|
// bundle whose wasm exceeds Cloudflare Pages' 25 MiB per-file limit.
|
|
12
12
|
import * as ort from 'onnxruntime-web/webgpu';
|
|
13
|
-
import { evictModelFromCache, fetchModelBlob,
|
|
13
|
+
import { evictModelFromCache, fetchModelBlob, loadPinnedTokenizer } from "../model-loader.js";
|
|
14
14
|
// ── Model config ──────────────────────────────────────────
|
|
15
15
|
// Supply-chain pinning (T116): the model is fetched from an immutable
|
|
16
16
|
// commit revision, never from mutable resolve/main, and the downloaded
|
|
@@ -26,6 +26,25 @@ export const GLINER_MODEL_REVISION = 'd21aad5b4a7ec82b3d0970fd1ac74a12c087d85e';
|
|
|
26
26
|
export const GLINER_MODEL_SHA256 = '891589426ee96f2748b16439f44fad8c3f97e198e002a6637e58dee989500216';
|
|
27
27
|
export const GLINER_MODEL_URL = `https://huggingface.co/knowledgator/gliner-pii-small-v1.0/resolve/${GLINER_MODEL_REVISION}/onnx/model_quint8.onnx`;
|
|
28
28
|
const DEFAULT_MODEL_URL = GLINER_MODEL_URL;
|
|
29
|
+
/**
|
|
30
|
+
* Tokenizer files at the same pinned commit (T185): fetched through
|
|
31
|
+
* fetchModelBlob with SHA-256 + size, cached next to the model and handed to
|
|
32
|
+
* CoreEnv.buildTokenizer. Hashes re-measured against the resolve/<commit>
|
|
33
|
+
* URLs on 2026-09-24 (documentation/model-provenance.md).
|
|
34
|
+
*/
|
|
35
|
+
export const GLINER_TOKENIZER_FILES = [
|
|
36
|
+
{
|
|
37
|
+
url: `https://huggingface.co/knowledgator/gliner-pii-small-v1.0/resolve/${GLINER_MODEL_REVISION}/tokenizer.json`,
|
|
38
|
+
sha256: '84b3a9b18f04a0ccd03b72d9f871b7e0bec40fd7021ef50bc30a7c3693c11205',
|
|
39
|
+
size: 3583593,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
url: `https://huggingface.co/knowledgator/gliner-pii-small-v1.0/resolve/${GLINER_MODEL_REVISION}/tokenizer_config.json`,
|
|
43
|
+
sha256: '3398f6d1ad4b4c4f9874d390d060a75c58cad5e5ce9b22841b3e40643b4ada27',
|
|
44
|
+
size: 21214,
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
const TOKENIZER_FILES = GLINER_TOKENIZER_FILES;
|
|
29
48
|
/**
|
|
30
49
|
* Superseded download URLs; their cache entries are evicted best-effort on
|
|
31
50
|
* load so stale ~46 MB copies do not linger against the origin quota:
|
|
@@ -36,6 +55,7 @@ const LEGACY_MODEL_URLS = [
|
|
|
36
55
|
'https://huggingface.co/knowledgator/gliner-pii-edge-v1.0/resolve/main/onnx/model_quint8.onnx',
|
|
37
56
|
'https://huggingface.co/knowledgator/gliner-pii-edge-v1.0/resolve/9b7f39b0a2da971a5beea78d35f1539d4009c891/onnx/model_quint8.onnx',
|
|
38
57
|
];
|
|
58
|
+
/** Hugging Face id for the deprecated CoreEnv.loadTokenizer fallback only. */
|
|
39
59
|
const DEFAULT_TOKENIZER_HF = 'knowledgator/gliner-pii-small-v1.0';
|
|
40
60
|
const DEFAULT_MODEL_NAME = 'GLiNER PII Small';
|
|
41
61
|
const CUSTOM_LABELS_STORAGE_KEY = 'doccloak-custom-labels';
|
|
@@ -236,7 +256,7 @@ export class GlinerProvider {
|
|
|
236
256
|
}),
|
|
237
257
|
];
|
|
238
258
|
if (!this.tokenizer) {
|
|
239
|
-
tasks.push(
|
|
259
|
+
tasks.push(loadPinnedTokenizer(this.env, TOKENIZER_FILES, DEFAULT_TOKENIZER_HF).then((t) => {
|
|
240
260
|
this.tokenizer = t;
|
|
241
261
|
}));
|
|
242
262
|
}
|
package/dist/regex/loader.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export interface RegexRuleJson {
|
|
|
31
31
|
falsePositiveNotes?: string;
|
|
32
32
|
/** Name of a post-match validation function in the validator registry */
|
|
33
33
|
validate?: string;
|
|
34
|
+
/** Run against the whole document instead of line by line (default false) */
|
|
35
|
+
multiline?: boolean;
|
|
34
36
|
}
|
|
35
37
|
/** JSON shape of a whole rules/<region>.json file. */
|
|
36
38
|
export interface RegionRulesJson {
|
package/dist/regex/loader.js
CHANGED
|
@@ -20,6 +20,8 @@ function compileRule(region, json) {
|
|
|
20
20
|
rule.examples = json.examples;
|
|
21
21
|
if (json.falsePositiveNotes !== undefined)
|
|
22
22
|
rule.falsePositiveNotes = json.falsePositiveNotes;
|
|
23
|
+
if (json.multiline !== undefined)
|
|
24
|
+
rule.multiline = json.multiline;
|
|
23
25
|
if (json.validate !== undefined) {
|
|
24
26
|
const validate = VALIDATORS[json.validate];
|
|
25
27
|
if (validate === undefined) {
|