@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.
- package/CHANGELOG.md +43 -0
- package/dist/dom/docx.d.ts +29 -2
- package/dist/dom/docx.js +64 -10
- package/dist/dom/index.d.ts +2 -0
- package/dist/dom/index.js +3 -0
- package/dist/dom/office.d.ts +75 -0
- package/dist/dom/office.js +116 -0
- package/dist/dom/opc.d.ts +42 -0
- package/dist/dom/opc.js +146 -0
- package/dist/dom/xlsx.d.ts +64 -0
- package/dist/dom/xlsx.js +280 -0
- package/dist/engine.d.ts +8 -5
- package/dist/engine.js +21 -7
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/providers/bardsai.js +16 -16
- package/dist/providers/gliner-base.d.ts +92 -0
- package/dist/providers/gliner-base.js +357 -0
- package/dist/providers/gliner.d.ts +22 -6
- package/dist/providers/gliner.js +43 -20
- package/package.json +3 -2
package/dist/dom/xlsx.js
ADDED
|
@@ -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
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* (
|
|
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
|
|
25
|
-
description: 'Lightweight, multi-language, supports custom labels (~
|
|
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
|
|
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
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
}
|