gheim 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -41,14 +41,15 @@ The core package itself has no runtime dependencies.
41
41
  (transformers.js). The package's default model is
42
42
  [`joelbarmettler/gheim-ch-560m`](https://huggingface.co/joelbarmettler/gheim-ch-560m)
43
43
  — a 560M xlm-roberta-large fine-tune optimised for Swiss-market PII
44
- (strict-span F1 0.916 on Swiss text, see
44
+ (test strict F1 0.910, char F1 0.946 on Swiss text, see
45
45
  [MODEL_CARD.md](https://github.com/joelbarmettlerUZH/gheim/blob/main/MODEL_CARD.md)).
46
46
  Any HuggingFace token-classification model that emits the same 33-class
47
47
  BIOES schema can be substituted via `model:`.
48
48
 
49
49
  | Model | Best for | Parameters | Notes |
50
50
  |---|---|---:|---|
51
- | [`joelbarmettler/gheim-ch-560m`](https://huggingface.co/joelbarmettler/gheim-ch-560m) **(default)** | Swiss-market text (de_CH, fr_CH, it_CH, rm, en) with CH-format account numbers (IBAN, AHV, VAT-CHE) | 560M | Apache 2.0. Test F1 0.916. Hub repo ships `onnx/model_quantized.onnx` only — load with `dtype: "q8"`. |
51
+ | [`joelbarmettler/gheim-ch-560m`](https://huggingface.co/joelbarmettler/gheim-ch-560m) **(default)** | Production / commercial. Swiss court / parliament / web text with CH-format account numbers (IBAN, AHV, VAT-CHE) | 560M | Apache 2.0. Test strict F1 0.910, char F1 0.946. Hub repo ships both fp32 and `onnx/model_quantized.onnx` — load with `dtype: "q8"` for browser deployment. |
52
+ | [`joelbarmettler/gheim-ch-560m-research`](https://huggingface.co/joelbarmettler/gheim-ch-560m-research) | Research / non-commercial. Stronger cross-domain transfer on Swiss-news text (swissner PER char F1 0.90 vs 0.70 on the default) | 560M | **CC BY-NC-SA 4.0 + Reuters research-only rider.** safetensors only — no browser-targeted ONNX. |
52
53
  | [`openai/privacy-filter`](https://huggingface.co/openai/privacy-filter) | English-first or general use, long-context (up to 128k tokens) | 1.4B (50M active, MoE) | Apache 2.0. Wider language coverage, larger weights. |
53
54
 
54
55
  ```ts
@@ -58,6 +59,9 @@ import { LocalDetector } from "gheim";
58
59
  // the browser and falls back to WASM; in Node it's WASM directly.
59
60
  const det = new LocalDetector({ dtype: "q8" });
60
61
 
62
+ // Stronger cross-domain transfer (research, non-commercial; safetensors only):
63
+ const detResearch = new LocalDetector({ model: "joelbarmettler/gheim-ch-560m-research" });
64
+
61
65
  // Alternative for English or general use:
62
66
  const detEn = new LocalDetector({ model: "openai/privacy-filter" });
63
67
  ```
@@ -157,8 +157,8 @@ class Session {
157
157
  }
158
158
  out.push(text.slice(cursor));
159
159
  let redacted = out.join("");
160
- redacted = recoverDuplicateOccurrences(redacted, this.mapping);
161
- return { redacted, merged };
160
+ const recovered = recoverDuplicateOccurrences(text, redacted, merged, this.mapping);
161
+ return { redacted: recovered.redacted, merged: recovered.merged };
162
162
  }
163
163
  restore(text) {
164
164
  if (Object.keys(this.mapping).length === 0)
@@ -217,14 +217,36 @@ function mergeAdjacent(text, spans) {
217
217
  function escapeRegex(s) {
218
218
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
219
219
  }
220
- function recoverDuplicateOccurrences(redacted, mapping) {
220
+ function recoverDuplicateOccurrences(text, redacted, merged, mapping) {
221
+ const surfaceToLabel = new Map;
222
+ for (const m of merged)
223
+ surfaceToLabel.set(text.slice(m.start, m.end), m.label);
224
+ const covered = merged.map((m) => [m.start, m.end]);
225
+ const overlaps = (s, e) => covered.some(([cs, ce]) => s < ce && e > cs);
221
226
  const entries = Object.entries(mapping).filter(([, surface]) => surface && surface.length > 0).sort((a, b) => b[1].length - a[1].length);
222
227
  let result = redacted;
228
+ const additional = [];
223
229
  for (const [sentinel, surface] of entries) {
224
- const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`, "gu");
225
- result = result.replace(pattern, sentinel);
230
+ const boundaryPattern = `(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`;
231
+ result = result.replace(new RegExp(boundaryPattern, "gu"), sentinel);
232
+ const label = surfaceToLabel.get(surface);
233
+ if (!label)
234
+ continue;
235
+ const origRe = new RegExp(boundaryPattern, "gu");
236
+ let match;
237
+ while ((match = origRe.exec(text)) !== null) {
238
+ const start = match.index;
239
+ const end = start + surface.length;
240
+ if (overlaps(start, end))
241
+ continue;
242
+ additional.push({ start, end, label });
243
+ covered.push([start, end]);
244
+ }
226
245
  }
227
- return result;
246
+ if (additional.length === 0)
247
+ return { redacted: result, merged };
248
+ const augmented = [...merged, ...additional].sort((a, b) => a.start - b.start);
249
+ return { redacted: result, merged: augmented };
228
250
  }
229
251
  // src/core/stream.ts
230
252
  var SENTINEL_AT = /<([A-Z][A-Z0-9_]*)_(\d+)>/y;
@@ -369,13 +391,14 @@ class LocalDetector {
369
391
  chunkOverlapTokens;
370
392
  batchSize;
371
393
  actualDevice = null;
394
+ actualDtype = null;
372
395
  _pipeline = null;
373
396
  _loadPromise = null;
374
397
  _onProgress;
375
398
  constructor(opts = {}) {
376
399
  this.model = opts.model ?? DEFAULT_MODEL2;
377
400
  this.device = opts.device ?? "auto";
378
- this.dtype = opts.dtype ?? "fp32";
401
+ this.dtype = opts.dtype ?? "auto";
379
402
  this.aggregationStrategy = opts.aggregationStrategy ?? "simple";
380
403
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
381
404
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
@@ -387,8 +410,14 @@ class LocalDetector {
387
410
  if (opts.pipelineInstance) {
388
411
  this._pipeline = opts.pipelineInstance;
389
412
  this.actualDevice = this.device === "auto" ? null : this.device;
413
+ this.actualDtype = this.dtype === "auto" ? null : this.dtype;
390
414
  }
391
415
  }
416
+ _resolveDtype(device) {
417
+ if (this.dtype !== "auto")
418
+ return this.dtype;
419
+ return device === "webgpu" ? "fp16" : "q8";
420
+ }
392
421
  async load() {
393
422
  if (this._pipeline)
394
423
  return this._pipeline;
@@ -400,14 +429,16 @@ class LocalDetector {
400
429
  let lastErr;
401
430
  for (let i = 0;i < sequence.length; i++) {
402
431
  const dev = sequence[i] ?? "wasm";
432
+ const resolvedDtype = this._resolveDtype(dev);
403
433
  try {
404
434
  const pipe = await mod.pipeline("token-classification", this.model, {
405
435
  device: dev,
406
- dtype: this.dtype,
436
+ dtype: resolvedDtype,
407
437
  progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
408
438
  });
409
439
  this._pipeline = pipe;
410
440
  this.actualDevice = dev;
441
+ this.actualDtype = resolvedDtype;
411
442
  return pipe;
412
443
  } catch (e) {
413
444
  lastErr = e;
@@ -423,6 +454,7 @@ class LocalDetector {
423
454
  async _planDeviceSequence() {
424
455
  if (this.device !== "auto")
425
456
  return [this.device];
457
+ const isBrowser = typeof globalThis.window !== "undefined";
426
458
  const seq = [];
427
459
  const nav = typeof navigator !== "undefined" ? navigator : null;
428
460
  if (nav?.gpu?.requestAdapter) {
@@ -432,7 +464,7 @@ class LocalDetector {
432
464
  seq.push("webgpu");
433
465
  } catch {}
434
466
  }
435
- seq.push("wasm");
467
+ seq.push(isBrowser ? "wasm" : "cpu");
436
468
  return seq;
437
469
  }
438
470
  _emitProgress(raw, device) {
@@ -864,6 +896,7 @@ class CalibratedDetector {
864
896
  chunkOverlapTokens;
865
897
  trimWhitespace;
866
898
  actualDevice = null;
899
+ actualDtype = null;
867
900
  _model = null;
868
901
  _tokenizer = null;
869
902
  _id2label = {};
@@ -874,7 +907,7 @@ class CalibratedDetector {
874
907
  this.model = opts.model ?? DEFAULT_MODEL3;
875
908
  this.oBias = opts.oBias ?? 0.5;
876
909
  this.device = opts.device ?? "auto";
877
- this.dtype = opts.dtype ?? "fp32";
910
+ this.dtype = opts.dtype ?? "auto";
878
911
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS2;
879
912
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS2;
880
913
  this.trimWhitespace = opts.trimWhitespace ?? true;
@@ -892,8 +925,9 @@ class CalibratedDetector {
892
925
  const mod = await loadTransformersJs2();
893
926
  let preferredDevice = this.device;
894
927
  if (this.device === "auto") {
928
+ const isBrowser = typeof globalThis.window !== "undefined";
895
929
  const nav = typeof navigator !== "undefined" ? navigator : null;
896
- preferredDevice = "wasm";
930
+ preferredDevice = isBrowser ? "wasm" : "cpu";
897
931
  if (nav?.gpu?.requestAdapter) {
898
932
  try {
899
933
  const adapter = await nav.gpu.requestAdapter();
@@ -902,14 +936,16 @@ class CalibratedDetector {
902
936
  } catch {}
903
937
  }
904
938
  }
939
+ const resolvedDtype = this.dtype === "auto" ? preferredDevice === "webgpu" ? "fp16" : "q8" : this.dtype;
905
940
  const tokenizer = await mod.AutoTokenizer.from_pretrained(this.model);
906
941
  const model = await mod.AutoModelForTokenClassification.from_pretrained(this.model, {
907
942
  device: preferredDevice,
908
- dtype: this.dtype,
943
+ dtype: resolvedDtype,
909
944
  progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, preferredDevice) : undefined
910
945
  });
911
946
  this._tokenizer = tokenizer;
912
947
  this._model = model;
948
+ this.actualDtype = resolvedDtype;
913
949
  this._id2label = model.config.id2label;
914
950
  const label2id = model.config.label2id;
915
951
  this._oId = label2id?.["O"] ?? 0;
@@ -1168,4 +1204,4 @@ async function* deanonymizeStream(chunks, session) {
1168
1204
  }
1169
1205
 
1170
1206
  // src/index.ts
1171
- var VERSION = "0.1.5";
1207
+ var VERSION = "0.1.6";
@@ -277,13 +277,14 @@ class LocalDetector {
277
277
  chunkOverlapTokens;
278
278
  batchSize;
279
279
  actualDevice = null;
280
+ actualDtype = null;
280
281
  _pipeline = null;
281
282
  _loadPromise = null;
282
283
  _onProgress;
283
284
  constructor(opts = {}) {
284
285
  this.model = opts.model ?? DEFAULT_MODEL2;
285
286
  this.device = opts.device ?? "auto";
286
- this.dtype = opts.dtype ?? "fp32";
287
+ this.dtype = opts.dtype ?? "auto";
287
288
  this.aggregationStrategy = opts.aggregationStrategy ?? "simple";
288
289
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
289
290
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
@@ -295,8 +296,14 @@ class LocalDetector {
295
296
  if (opts.pipelineInstance) {
296
297
  this._pipeline = opts.pipelineInstance;
297
298
  this.actualDevice = this.device === "auto" ? null : this.device;
299
+ this.actualDtype = this.dtype === "auto" ? null : this.dtype;
298
300
  }
299
301
  }
302
+ _resolveDtype(device) {
303
+ if (this.dtype !== "auto")
304
+ return this.dtype;
305
+ return device === "webgpu" ? "fp16" : "q8";
306
+ }
300
307
  async load() {
301
308
  if (this._pipeline)
302
309
  return this._pipeline;
@@ -308,14 +315,16 @@ class LocalDetector {
308
315
  let lastErr;
309
316
  for (let i = 0;i < sequence.length; i++) {
310
317
  const dev = sequence[i] ?? "wasm";
318
+ const resolvedDtype = this._resolveDtype(dev);
311
319
  try {
312
320
  const pipe = await mod.pipeline("token-classification", this.model, {
313
321
  device: dev,
314
- dtype: this.dtype,
322
+ dtype: resolvedDtype,
315
323
  progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
316
324
  });
317
325
  this._pipeline = pipe;
318
326
  this.actualDevice = dev;
327
+ this.actualDtype = resolvedDtype;
319
328
  return pipe;
320
329
  } catch (e) {
321
330
  lastErr = e;
@@ -331,6 +340,7 @@ class LocalDetector {
331
340
  async _planDeviceSequence() {
332
341
  if (this.device !== "auto")
333
342
  return [this.device];
343
+ const isBrowser = typeof globalThis.window !== "undefined";
334
344
  const seq = [];
335
345
  const nav = typeof navigator !== "undefined" ? navigator : null;
336
346
  if (nav?.gpu?.requestAdapter) {
@@ -340,7 +350,7 @@ class LocalDetector {
340
350
  seq.push("webgpu");
341
351
  } catch {}
342
352
  }
343
- seq.push("wasm");
353
+ seq.push(isBrowser ? "wasm" : "cpu");
344
354
  return seq;
345
355
  }
346
356
  _emitProgress(raw, device) {
@@ -772,6 +782,7 @@ class CalibratedDetector {
772
782
  chunkOverlapTokens;
773
783
  trimWhitespace;
774
784
  actualDevice = null;
785
+ actualDtype = null;
775
786
  _model = null;
776
787
  _tokenizer = null;
777
788
  _id2label = {};
@@ -782,7 +793,7 @@ class CalibratedDetector {
782
793
  this.model = opts.model ?? DEFAULT_MODEL3;
783
794
  this.oBias = opts.oBias ?? 0.5;
784
795
  this.device = opts.device ?? "auto";
785
- this.dtype = opts.dtype ?? "fp32";
796
+ this.dtype = opts.dtype ?? "auto";
786
797
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS2;
787
798
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS2;
788
799
  this.trimWhitespace = opts.trimWhitespace ?? true;
@@ -800,8 +811,9 @@ class CalibratedDetector {
800
811
  const mod = await loadTransformersJs2();
801
812
  let preferredDevice = this.device;
802
813
  if (this.device === "auto") {
814
+ const isBrowser = typeof globalThis.window !== "undefined";
803
815
  const nav = typeof navigator !== "undefined" ? navigator : null;
804
- preferredDevice = "wasm";
816
+ preferredDevice = isBrowser ? "wasm" : "cpu";
805
817
  if (nav?.gpu?.requestAdapter) {
806
818
  try {
807
819
  const adapter = await nav.gpu.requestAdapter();
@@ -810,14 +822,16 @@ class CalibratedDetector {
810
822
  } catch {}
811
823
  }
812
824
  }
825
+ const resolvedDtype = this.dtype === "auto" ? preferredDevice === "webgpu" ? "fp16" : "q8" : this.dtype;
813
826
  const tokenizer = await mod.AutoTokenizer.from_pretrained(this.model);
814
827
  const model = await mod.AutoModelForTokenClassification.from_pretrained(this.model, {
815
828
  device: preferredDevice,
816
- dtype: this.dtype,
829
+ dtype: resolvedDtype,
817
830
  progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, preferredDevice) : undefined
818
831
  });
819
832
  this._tokenizer = tokenizer;
820
833
  this._model = model;
834
+ this.actualDtype = resolvedDtype;
821
835
  this._id2label = model.config.id2label;
822
836
  const label2id = model.config.label2id;
823
837
  this._oId = label2id?.["O"] ?? 0;
@@ -1086,8 +1100,8 @@ class Session {
1086
1100
  }
1087
1101
  out.push(text.slice(cursor));
1088
1102
  let redacted = out.join("");
1089
- redacted = recoverDuplicateOccurrences(redacted, this.mapping);
1090
- return { redacted, merged };
1103
+ const recovered = recoverDuplicateOccurrences(text, redacted, merged, this.mapping);
1104
+ return { redacted: recovered.redacted, merged: recovered.merged };
1091
1105
  }
1092
1106
  restore(text) {
1093
1107
  if (Object.keys(this.mapping).length === 0)
@@ -1146,14 +1160,36 @@ function mergeAdjacent(text, spans) {
1146
1160
  function escapeRegex(s) {
1147
1161
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1148
1162
  }
1149
- function recoverDuplicateOccurrences(redacted, mapping) {
1163
+ function recoverDuplicateOccurrences(text, redacted, merged, mapping) {
1164
+ const surfaceToLabel = new Map;
1165
+ for (const m of merged)
1166
+ surfaceToLabel.set(text.slice(m.start, m.end), m.label);
1167
+ const covered = merged.map((m) => [m.start, m.end]);
1168
+ const overlaps = (s, e) => covered.some(([cs, ce]) => s < ce && e > cs);
1150
1169
  const entries = Object.entries(mapping).filter(([, surface]) => surface && surface.length > 0).sort((a, b) => b[1].length - a[1].length);
1151
1170
  let result = redacted;
1171
+ const additional = [];
1152
1172
  for (const [sentinel, surface] of entries) {
1153
- const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`, "gu");
1154
- result = result.replace(pattern, sentinel);
1173
+ const boundaryPattern = `(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`;
1174
+ result = result.replace(new RegExp(boundaryPattern, "gu"), sentinel);
1175
+ const label = surfaceToLabel.get(surface);
1176
+ if (!label)
1177
+ continue;
1178
+ const origRe = new RegExp(boundaryPattern, "gu");
1179
+ let match;
1180
+ while ((match = origRe.exec(text)) !== null) {
1181
+ const start = match.index;
1182
+ const end = start + surface.length;
1183
+ if (overlaps(start, end))
1184
+ continue;
1185
+ additional.push({ start, end, label });
1186
+ covered.push([start, end]);
1187
+ }
1155
1188
  }
1156
- return result;
1189
+ if (additional.length === 0)
1190
+ return { redacted: result, merged };
1191
+ const augmented = [...merged, ...additional].sort((a, b) => a.start - b.start);
1192
+ return { redacted: result, merged: augmented };
1157
1193
  }
1158
1194
 
1159
1195
  // src/openai/_base.ts
package/dist/esm/index.js CHANGED
@@ -100,8 +100,8 @@ class Session {
100
100
  }
101
101
  out.push(text.slice(cursor));
102
102
  let redacted = out.join("");
103
- redacted = recoverDuplicateOccurrences(redacted, this.mapping);
104
- return { redacted, merged };
103
+ const recovered = recoverDuplicateOccurrences(text, redacted, merged, this.mapping);
104
+ return { redacted: recovered.redacted, merged: recovered.merged };
105
105
  }
106
106
  restore(text) {
107
107
  if (Object.keys(this.mapping).length === 0)
@@ -160,14 +160,36 @@ function mergeAdjacent(text, spans) {
160
160
  function escapeRegex(s) {
161
161
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
162
162
  }
163
- function recoverDuplicateOccurrences(redacted, mapping) {
163
+ function recoverDuplicateOccurrences(text, redacted, merged, mapping) {
164
+ const surfaceToLabel = new Map;
165
+ for (const m of merged)
166
+ surfaceToLabel.set(text.slice(m.start, m.end), m.label);
167
+ const covered = merged.map((m) => [m.start, m.end]);
168
+ const overlaps = (s, e) => covered.some(([cs, ce]) => s < ce && e > cs);
164
169
  const entries = Object.entries(mapping).filter(([, surface]) => surface && surface.length > 0).sort((a, b) => b[1].length - a[1].length);
165
170
  let result = redacted;
171
+ const additional = [];
166
172
  for (const [sentinel, surface] of entries) {
167
- const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`, "gu");
168
- result = result.replace(pattern, sentinel);
173
+ const boundaryPattern = `(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`;
174
+ result = result.replace(new RegExp(boundaryPattern, "gu"), sentinel);
175
+ const label = surfaceToLabel.get(surface);
176
+ if (!label)
177
+ continue;
178
+ const origRe = new RegExp(boundaryPattern, "gu");
179
+ let match;
180
+ while ((match = origRe.exec(text)) !== null) {
181
+ const start = match.index;
182
+ const end = start + surface.length;
183
+ if (overlaps(start, end))
184
+ continue;
185
+ additional.push({ start, end, label });
186
+ covered.push([start, end]);
187
+ }
169
188
  }
170
- return result;
189
+ if (additional.length === 0)
190
+ return { redacted: result, merged };
191
+ const augmented = [...merged, ...additional].sort((a, b) => a.start - b.start);
192
+ return { redacted: result, merged: augmented };
171
193
  }
172
194
  // src/core/stream.ts
173
195
  var SENTINEL_AT = /<([A-Z][A-Z0-9_]*)_(\d+)>/y;
@@ -312,13 +334,14 @@ class LocalDetector {
312
334
  chunkOverlapTokens;
313
335
  batchSize;
314
336
  actualDevice = null;
337
+ actualDtype = null;
315
338
  _pipeline = null;
316
339
  _loadPromise = null;
317
340
  _onProgress;
318
341
  constructor(opts = {}) {
319
342
  this.model = opts.model ?? DEFAULT_MODEL2;
320
343
  this.device = opts.device ?? "auto";
321
- this.dtype = opts.dtype ?? "fp32";
344
+ this.dtype = opts.dtype ?? "auto";
322
345
  this.aggregationStrategy = opts.aggregationStrategy ?? "simple";
323
346
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
324
347
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
@@ -330,8 +353,14 @@ class LocalDetector {
330
353
  if (opts.pipelineInstance) {
331
354
  this._pipeline = opts.pipelineInstance;
332
355
  this.actualDevice = this.device === "auto" ? null : this.device;
356
+ this.actualDtype = this.dtype === "auto" ? null : this.dtype;
333
357
  }
334
358
  }
359
+ _resolveDtype(device) {
360
+ if (this.dtype !== "auto")
361
+ return this.dtype;
362
+ return device === "webgpu" ? "fp16" : "q8";
363
+ }
335
364
  async load() {
336
365
  if (this._pipeline)
337
366
  return this._pipeline;
@@ -343,14 +372,16 @@ class LocalDetector {
343
372
  let lastErr;
344
373
  for (let i = 0;i < sequence.length; i++) {
345
374
  const dev = sequence[i] ?? "wasm";
375
+ const resolvedDtype = this._resolveDtype(dev);
346
376
  try {
347
377
  const pipe = await mod.pipeline("token-classification", this.model, {
348
378
  device: dev,
349
- dtype: this.dtype,
379
+ dtype: resolvedDtype,
350
380
  progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
351
381
  });
352
382
  this._pipeline = pipe;
353
383
  this.actualDevice = dev;
384
+ this.actualDtype = resolvedDtype;
354
385
  return pipe;
355
386
  } catch (e) {
356
387
  lastErr = e;
@@ -366,6 +397,7 @@ class LocalDetector {
366
397
  async _planDeviceSequence() {
367
398
  if (this.device !== "auto")
368
399
  return [this.device];
400
+ const isBrowser = typeof globalThis.window !== "undefined";
369
401
  const seq = [];
370
402
  const nav = typeof navigator !== "undefined" ? navigator : null;
371
403
  if (nav?.gpu?.requestAdapter) {
@@ -375,7 +407,7 @@ class LocalDetector {
375
407
  seq.push("webgpu");
376
408
  } catch {}
377
409
  }
378
- seq.push("wasm");
410
+ seq.push(isBrowser ? "wasm" : "cpu");
379
411
  return seq;
380
412
  }
381
413
  _emitProgress(raw, device) {
@@ -807,6 +839,7 @@ class CalibratedDetector {
807
839
  chunkOverlapTokens;
808
840
  trimWhitespace;
809
841
  actualDevice = null;
842
+ actualDtype = null;
810
843
  _model = null;
811
844
  _tokenizer = null;
812
845
  _id2label = {};
@@ -817,7 +850,7 @@ class CalibratedDetector {
817
850
  this.model = opts.model ?? DEFAULT_MODEL3;
818
851
  this.oBias = opts.oBias ?? 0.5;
819
852
  this.device = opts.device ?? "auto";
820
- this.dtype = opts.dtype ?? "fp32";
853
+ this.dtype = opts.dtype ?? "auto";
821
854
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS2;
822
855
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS2;
823
856
  this.trimWhitespace = opts.trimWhitespace ?? true;
@@ -835,8 +868,9 @@ class CalibratedDetector {
835
868
  const mod = await loadTransformersJs2();
836
869
  let preferredDevice = this.device;
837
870
  if (this.device === "auto") {
871
+ const isBrowser = typeof globalThis.window !== "undefined";
838
872
  const nav = typeof navigator !== "undefined" ? navigator : null;
839
- preferredDevice = "wasm";
873
+ preferredDevice = isBrowser ? "wasm" : "cpu";
840
874
  if (nav?.gpu?.requestAdapter) {
841
875
  try {
842
876
  const adapter = await nav.gpu.requestAdapter();
@@ -845,14 +879,16 @@ class CalibratedDetector {
845
879
  } catch {}
846
880
  }
847
881
  }
882
+ const resolvedDtype = this.dtype === "auto" ? preferredDevice === "webgpu" ? "fp16" : "q8" : this.dtype;
848
883
  const tokenizer = await mod.AutoTokenizer.from_pretrained(this.model);
849
884
  const model = await mod.AutoModelForTokenClassification.from_pretrained(this.model, {
850
885
  device: preferredDevice,
851
- dtype: this.dtype,
886
+ dtype: resolvedDtype,
852
887
  progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, preferredDevice) : undefined
853
888
  });
854
889
  this._tokenizer = tokenizer;
855
890
  this._model = model;
891
+ this.actualDtype = resolvedDtype;
856
892
  this._id2label = model.config.id2label;
857
893
  const label2id = model.config.label2id;
858
894
  this._oId = label2id?.["O"] ?? 0;
@@ -1111,7 +1147,7 @@ async function* deanonymizeStream(chunks, session) {
1111
1147
  }
1112
1148
 
1113
1149
  // src/index.ts
1114
- var VERSION = "0.1.5";
1150
+ var VERSION = "0.1.6";
1115
1151
  export {
1116
1152
  renderSentinel,
1117
1153
  parseSentinel,
@@ -234,13 +234,14 @@ class LocalDetector {
234
234
  chunkOverlapTokens;
235
235
  batchSize;
236
236
  actualDevice = null;
237
+ actualDtype = null;
237
238
  _pipeline = null;
238
239
  _loadPromise = null;
239
240
  _onProgress;
240
241
  constructor(opts = {}) {
241
242
  this.model = opts.model ?? DEFAULT_MODEL2;
242
243
  this.device = opts.device ?? "auto";
243
- this.dtype = opts.dtype ?? "fp32";
244
+ this.dtype = opts.dtype ?? "auto";
244
245
  this.aggregationStrategy = opts.aggregationStrategy ?? "simple";
245
246
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
246
247
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
@@ -252,8 +253,14 @@ class LocalDetector {
252
253
  if (opts.pipelineInstance) {
253
254
  this._pipeline = opts.pipelineInstance;
254
255
  this.actualDevice = this.device === "auto" ? null : this.device;
256
+ this.actualDtype = this.dtype === "auto" ? null : this.dtype;
255
257
  }
256
258
  }
259
+ _resolveDtype(device) {
260
+ if (this.dtype !== "auto")
261
+ return this.dtype;
262
+ return device === "webgpu" ? "fp16" : "q8";
263
+ }
257
264
  async load() {
258
265
  if (this._pipeline)
259
266
  return this._pipeline;
@@ -265,14 +272,16 @@ class LocalDetector {
265
272
  let lastErr;
266
273
  for (let i = 0;i < sequence.length; i++) {
267
274
  const dev = sequence[i] ?? "wasm";
275
+ const resolvedDtype = this._resolveDtype(dev);
268
276
  try {
269
277
  const pipe = await mod.pipeline("token-classification", this.model, {
270
278
  device: dev,
271
- dtype: this.dtype,
279
+ dtype: resolvedDtype,
272
280
  progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
273
281
  });
274
282
  this._pipeline = pipe;
275
283
  this.actualDevice = dev;
284
+ this.actualDtype = resolvedDtype;
276
285
  return pipe;
277
286
  } catch (e) {
278
287
  lastErr = e;
@@ -288,6 +297,7 @@ class LocalDetector {
288
297
  async _planDeviceSequence() {
289
298
  if (this.device !== "auto")
290
299
  return [this.device];
300
+ const isBrowser = typeof globalThis.window !== "undefined";
291
301
  const seq = [];
292
302
  const nav = typeof navigator !== "undefined" ? navigator : null;
293
303
  if (nav?.gpu?.requestAdapter) {
@@ -297,7 +307,7 @@ class LocalDetector {
297
307
  seq.push("webgpu");
298
308
  } catch {}
299
309
  }
300
- seq.push("wasm");
310
+ seq.push(isBrowser ? "wasm" : "cpu");
301
311
  return seq;
302
312
  }
303
313
  _emitProgress(raw, device) {
@@ -729,6 +739,7 @@ class CalibratedDetector {
729
739
  chunkOverlapTokens;
730
740
  trimWhitespace;
731
741
  actualDevice = null;
742
+ actualDtype = null;
732
743
  _model = null;
733
744
  _tokenizer = null;
734
745
  _id2label = {};
@@ -739,7 +750,7 @@ class CalibratedDetector {
739
750
  this.model = opts.model ?? DEFAULT_MODEL3;
740
751
  this.oBias = opts.oBias ?? 0.5;
741
752
  this.device = opts.device ?? "auto";
742
- this.dtype = opts.dtype ?? "fp32";
753
+ this.dtype = opts.dtype ?? "auto";
743
754
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS2;
744
755
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS2;
745
756
  this.trimWhitespace = opts.trimWhitespace ?? true;
@@ -757,8 +768,9 @@ class CalibratedDetector {
757
768
  const mod = await loadTransformersJs2();
758
769
  let preferredDevice = this.device;
759
770
  if (this.device === "auto") {
771
+ const isBrowser = typeof globalThis.window !== "undefined";
760
772
  const nav = typeof navigator !== "undefined" ? navigator : null;
761
- preferredDevice = "wasm";
773
+ preferredDevice = isBrowser ? "wasm" : "cpu";
762
774
  if (nav?.gpu?.requestAdapter) {
763
775
  try {
764
776
  const adapter = await nav.gpu.requestAdapter();
@@ -767,14 +779,16 @@ class CalibratedDetector {
767
779
  } catch {}
768
780
  }
769
781
  }
782
+ const resolvedDtype = this.dtype === "auto" ? preferredDevice === "webgpu" ? "fp16" : "q8" : this.dtype;
770
783
  const tokenizer = await mod.AutoTokenizer.from_pretrained(this.model);
771
784
  const model = await mod.AutoModelForTokenClassification.from_pretrained(this.model, {
772
785
  device: preferredDevice,
773
- dtype: this.dtype,
786
+ dtype: resolvedDtype,
774
787
  progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, preferredDevice) : undefined
775
788
  });
776
789
  this._tokenizer = tokenizer;
777
790
  this._model = model;
791
+ this.actualDtype = resolvedDtype;
778
792
  this._id2label = model.config.id2label;
779
793
  const label2id = model.config.label2id;
780
794
  this._oId = label2id?.["O"] ?? 0;
@@ -1043,8 +1057,8 @@ class Session {
1043
1057
  }
1044
1058
  out.push(text.slice(cursor));
1045
1059
  let redacted = out.join("");
1046
- redacted = recoverDuplicateOccurrences(redacted, this.mapping);
1047
- return { redacted, merged };
1060
+ const recovered = recoverDuplicateOccurrences(text, redacted, merged, this.mapping);
1061
+ return { redacted: recovered.redacted, merged: recovered.merged };
1048
1062
  }
1049
1063
  restore(text) {
1050
1064
  if (Object.keys(this.mapping).length === 0)
@@ -1103,14 +1117,36 @@ function mergeAdjacent(text, spans) {
1103
1117
  function escapeRegex(s) {
1104
1118
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1105
1119
  }
1106
- function recoverDuplicateOccurrences(redacted, mapping) {
1120
+ function recoverDuplicateOccurrences(text, redacted, merged, mapping) {
1121
+ const surfaceToLabel = new Map;
1122
+ for (const m of merged)
1123
+ surfaceToLabel.set(text.slice(m.start, m.end), m.label);
1124
+ const covered = merged.map((m) => [m.start, m.end]);
1125
+ const overlaps = (s, e) => covered.some(([cs, ce]) => s < ce && e > cs);
1107
1126
  const entries = Object.entries(mapping).filter(([, surface]) => surface && surface.length > 0).sort((a, b) => b[1].length - a[1].length);
1108
1127
  let result = redacted;
1128
+ const additional = [];
1109
1129
  for (const [sentinel, surface] of entries) {
1110
- const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`, "gu");
1111
- result = result.replace(pattern, sentinel);
1130
+ const boundaryPattern = `(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`;
1131
+ result = result.replace(new RegExp(boundaryPattern, "gu"), sentinel);
1132
+ const label = surfaceToLabel.get(surface);
1133
+ if (!label)
1134
+ continue;
1135
+ const origRe = new RegExp(boundaryPattern, "gu");
1136
+ let match;
1137
+ while ((match = origRe.exec(text)) !== null) {
1138
+ const start = match.index;
1139
+ const end = start + surface.length;
1140
+ if (overlaps(start, end))
1141
+ continue;
1142
+ additional.push({ start, end, label });
1143
+ covered.push([start, end]);
1144
+ }
1112
1145
  }
1113
- return result;
1146
+ if (additional.length === 0)
1147
+ return { redacted: result, merged };
1148
+ const augmented = [...merged, ...additional].sort((a, b) => a.start - b.start);
1149
+ return { redacted: result, merged: augmented };
1114
1150
  }
1115
1151
 
1116
1152
  // src/openai/_base.ts
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/session.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAGjD;;;gEAGgE;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAkBtD,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC1C;AAED,qBAAa,OAAO;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IACrC,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,mBAAmB,CAAkC;gBAEjD,IAAI,GAAE,cAAmB;IAQrC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAiBhD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GAAG,MAAM;IAIvE;;;;;;;OAOG;IACH,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GACpC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;IA+B7C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAK7B,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAI7C,MAAM,CAAC,QAAQ,CACb,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EACxC,IAAI,GAAE,cAAmB,GACxB,OAAO;CAqBX;AAID,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GACpC,UAAU,EAAE,CAqBd"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../../src/core/session.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAGjD;;;gEAGgE;AAChE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAkBtD,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CAC1C;AAED,qBAAa,OAAO;IAClB,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAM;IACrC,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,QAAQ,CAAkC;IAClD,OAAO,CAAC,mBAAmB,CAAkC;gBAEjD,IAAI,GAAE,cAAmB;IAQrC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAiBhD,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GAAG,MAAM;IAIvE;;;;;;;OAOG;IACH,kBAAkB,CAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GACpC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,UAAU,EAAE,CAAA;KAAE;IA8B7C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;IAK7B,MAAM,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE;IAI7C,MAAM,CAAC,QAAQ,CACb,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,EACxC,IAAI,GAAE,cAAmB,GACxB,OAAO;CAqBX;AAID,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,CAAC,IAAI,GAAG,UAAU,CAAC,EAAE,GACpC,UAAU,EAAE,CAqBd"}
@@ -1,19 +1,27 @@
1
1
  /**
2
- * Calibrated detector — fixes the v1 model's "single-category mode"
3
- * bug at inference time by subtracting a constant `oBias` from the O
4
- * class logit before argmax.
2
+ * Calibrated detector — subtracts a constant `oBias` from the O-class
3
+ * logit before argmax. Originally introduced to recover the v1 model's
4
+ * "single-category mode" pathology (Müller + IBAN co-occurrence flipped
5
+ * the person prediction to O); the v3 / gheim-ch-560m training pipeline
6
+ * largely resolved that pathology, but the calibration still buys a
7
+ * small precision/recall improvement and remains the default.
5
8
  *
6
- * Empirical justification (see eval/calibration_sweep.json): on a
7
- * 237-case multi-entity pathology suite the v1 model emits literally
8
- * zero non-O tokens for "Müller" / "März" when an IBAN follows them —
9
- * but the per-token softmax shows person at 47% vs O at 51%. A small
10
- * additive bias on O flips those marginal cases without inducing false
11
- * positives on PII-free text.
9
+ * Sweep on gheim-ch-560m (see eval/calibration_sweep.json):
10
+ * `oBias=0.5` is the Pareto-clean default — probe perfect-rate is flat
11
+ * at 91.5% across bias 0.0 and 0.5, and test char-F1 lifts by 0.27pp.
12
+ * Higher biases (1.0–1.5) trade test F1 for marginal probe gains.
12
13
  *
13
- * Sweet spot: oBias=0.5:
14
- * - pathology full-coverage 81% → 94.5% (+13.5pp)
15
- * - test_v1 strict-F1 0.872 → 0.868 (−0.4pp)
16
- * - ~0.05 extra FP per chunk on real text
14
+ * A separate sweep on the int8 ONNX path via `transformers.js`
15
+ * (eval/calibration_sweep_q8.json) showed a much larger calibration
16
+ * effect (+1.4pp probe, +0.43pp char-F1 at 0.5). That gap is not a
17
+ * property of the int8 quantisation — Python `onnxruntime` on the same
18
+ * int8 file gives 91.0% probe perfect-rate, essentially fp32-equivalent
19
+ * — but of a runtime divergence inside `onnxruntime-web` / WASM that
20
+ * the calibration partially papers over. The reliable fix is to use the
21
+ * `model_fp16.onnx` variant when WebGPU is available (which is what
22
+ * LocalDetector's default `dtype: "auto"` does); int8 should be treated
23
+ * as a low-RAM fallback. See eval/q8_quality_report.md for the
24
+ * diagnostic.
17
25
  *
18
26
  * `defaultDetector()` returns this with oBias=0.5, so users get the
19
27
  * better behavior automatically.
@@ -26,14 +34,13 @@ export interface CalibratedDetectorOptions {
26
34
  model?: string;
27
35
  /**
28
36
  * Constant subtracted from the O-class logit before argmax. Default
29
- * `0.5` is the Pareto-clean point per the full-scale bias sweep
30
- * (eval/calibration_sweep.json):
37
+ * `0.5` is the Pareto-clean point on the reference (fp32/fp16) path:
31
38
  *
32
39
  * - oBias=0.0 uncalibrated (matches the raw model)
33
- * - oBias=0.5 recovers ~95% of multi-entity pathology cases for
34
- * ~0.4pp test_v1 strict-F1 cost
35
- * - oBias=1.0 more aggressive: 96.6% pathology, 1.1pp test_v1 cost
36
- * - oBias=1.5+ trades too much precision; not recommended
40
+ * - oBias=0.5 +0.27pp test char-F1, no probe cost (91.5% perfect)
41
+ * - oBias=1.0+ marginal probe gains, test F1 starts to fall off
42
+ *
43
+ * See eval/calibration_sweep.json.
37
44
  */
38
45
  oBias?: number;
39
46
  /** Backend selector. See {@link LocalDetectorOptions.device}. */
@@ -60,6 +67,9 @@ export declare class CalibratedDetector implements Detector {
60
67
  readonly trimWhitespace: boolean;
61
68
  /** Backend actually used after `load()` resolves; `null` before. */
62
69
  actualDevice: string | null;
70
+ /** Dtype actually used after `load()` resolves; `null` before. Differs
71
+ * from {@link dtype} only when `dtype: "auto"` was requested. */
72
+ actualDtype: string | null;
63
73
  private _model;
64
74
  private _tokenizer;
65
75
  private _id2label;
@@ -1 +1 @@
1
- {"version":3,"file":"calibrated.d.ts","sourceRoot":"","sources":["../../../src/detectors/calibrated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAMzD,0DAA0D;AAC1D,MAAM,WAAW,yBAAyB;IACxC,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;OAUG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACtD;AA4CD,uDAAuD;AACvD,qBAAa,kBAAmB,YAAW,QAAQ;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,oEAAoE;IACpE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IACnC,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAA0C;gBAE3D,IAAI,GAAE,yBAA8B;IAiB1C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAuCrB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;YAYxD,cAAc;YA0Bd,aAAa;IA6F3B,OAAO,CAAC,aAAa;CAiBtB"}
1
+ {"version":3,"file":"calibrated.d.ts","sourceRoot":"","sources":["../../../src/detectors/calibrated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAMzD,0DAA0D;AAC1D,MAAM,WAAW,yBAAyB;IACxC,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACtD;AA4CD,uDAAuD;AACvD,qBAAa,kBAAmB,YAAW,QAAQ;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,oEAAoE;IACpE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IACnC;sEACkE;IAClE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAClC,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAA0C;gBAE3D,IAAI,GAAE,yBAA8B;IAiB1C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoDrB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;YAYxD,cAAc;YA0Bd,aAAa;IA6F3B,OAAO,CAAC,aAAa;CAiBtB"}
@@ -11,8 +11,8 @@ import type { Detector } from "./base.ts";
11
11
  /** Pick a reasonable default: remote when `GHEIM_API_KEY` is set in env,
12
12
  * else a calibrated local model.
13
13
  *
14
- * The calibrated default (`oBias=0.5`) recovers ~95% of the
15
- * multi-entity-letter cases that the raw v1 model fails on, with
16
- * ~0.4pp test_v1 strict-F1 cost. See eval/calibration_sweep.json. */
14
+ * The calibrated default (`oBias=0.5`) is Pareto-clean on gheim-ch-560m:
15
+ * +0.27pp test char-F1 with no probe cost (91.5% perfect at both 0.0
16
+ * and 0.5). See eval/calibration_sweep.json. */
17
17
  export declare function defaultDetector(): Detector;
18
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EACL,gBAAgB,EAChB,aAAa,IAAI,oBAAoB,EACrC,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,YAAY,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAI1C;;;;;sEAKsE;AACtE,wBAAgB,eAAe,IAAI,QAAQ,CAI1C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EACL,gBAAgB,EAChB,aAAa,IAAI,oBAAoB,EACrC,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,YAAY,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AACrD,YAAY,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAC;AAEjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAI1C;;;;;iDAKiD;AACjD,wBAAgB,eAAe,IAAI,QAAQ,CAI1C"}
@@ -29,7 +29,15 @@ export interface LocalDetectorOptions {
29
29
  * (no probe, no fallback — your choice is respected and may throw).
30
30
  */
31
31
  device?: string;
32
- /** Quantization dtype: "fp32" | "fp16" | "q8" | "q4" | "auto" */
32
+ /**
33
+ * Quantization dtype: `"fp32" | "fp16" | "q8" | "q4" | "auto"`. Defaults
34
+ * to `"auto"`, which picks based on the resolved backend: `"fp16"` when
35
+ * WebGPU was selected (large but byte-equivalent to fp32 on the forensic
36
+ * probe), `"q8"` otherwise (~550 MB; safe everywhere, but degrades on
37
+ * the `onnxruntime-web` WASM kernel on Swiss edge cases — see
38
+ * eval/q8_quality_report.md). Set explicitly to opt out of this
39
+ * trade-off.
40
+ */
33
41
  dtype?: string;
34
42
  /** Aggregation strategy passed to the pipeline call. */
35
43
  aggregationStrategy?: string;
@@ -98,10 +106,21 @@ export declare class LocalDetector implements Detector {
98
106
  * the model is loaded. Different from {@link device} only when
99
107
  * `device: "auto"` was requested. */
100
108
  actualDevice: string | null;
109
+ /** Dtype that ended up being used after `load()` resolves. `null` until
110
+ * the model is loaded. Different from {@link dtype} only when
111
+ * `dtype: "auto"` was requested — in that case it resolves to `"fp16"`
112
+ * on WebGPU and `"q8"` everywhere else. */
113
+ actualDtype: string | null;
101
114
  private _pipeline;
102
115
  private _loadPromise;
103
116
  private readonly _onProgress?;
104
117
  constructor(opts?: LocalDetectorOptions);
118
+ /** Resolve `dtype: "auto"` for a concrete device: fp16 on WebGPU (byte-
119
+ * equivalent to fp32 on the forensic probe), q8 on WASM/CPU. The
120
+ * WASM-q8 path is known to degrade on Swiss edge cases via
121
+ * `onnxruntime-web`, but it's the small-file fallback for low-RAM
122
+ * contexts where WebGPU isn't available. Explicit dtype passes through. */
123
+ private _resolveDtype;
105
124
  /**
106
125
  * Load the model. Idempotent; concurrent calls share a single load.
107
126
  * With `device: "auto"`, probes `navigator.gpu.requestAdapter()` and
@@ -109,7 +128,15 @@ export declare class LocalDetector implements Detector {
109
128
  * if construction throws (and reporting both attempts via `onProgress`).
110
129
  */
111
130
  load(): Promise<PipelineCallable>;
112
- /** Build the ordered list of backends to attempt. */
131
+ /** Build the ordered list of backends to attempt.
132
+ *
133
+ * In a browser we try WebGPU (if a real adapter is reachable) and fall
134
+ * back to WASM. In Node, transformers.js uses onnxruntime-node, which
135
+ * speaks "cpu" / "cuda" / "webgpu" — never "wasm" — so the fallback
136
+ * has to be "cpu" instead. We detect "browser-ness" via `typeof window`;
137
+ * Node 21+ now ships a global `navigator`, so that's no longer a
138
+ * reliable discriminator on its own.
139
+ */
113
140
  private _planDeviceSequence;
114
141
  /** Translate a raw transformers.js progress callback into our normalized event. */
115
142
  private _emitProgress;
@@ -1 +1 @@
1
- {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/detectors/local.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../../src/detectors/local.ts"],"names":[],"mappings":"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"}
@@ -17,7 +17,7 @@ export { DEFAULT_BASE_URL, REMOTE_DEFAULT_MODEL, RemoteDetector, LocalDetector,
17
17
  export type { RemoteDetectorOptions, LocalDetectorOptions, LocalDetectorLoadEvent, CompositeDetectorOptions, CalibratedDetectorOptions, } from "./detectors/index.ts";
18
18
  export { anonymizeMessages, anonymizeText, deanonymizeStream, deanonymizeText, } from "./plain.ts";
19
19
  export type { ChatMessage } from "./plain.ts";
20
- export declare const VERSION = "0.1.5";
20
+ export declare const VERSION = "0.1.6";
21
21
  export { mergeAdjacent } from "./core/session.ts";
22
22
  export type { MergedSpan } from "./core/session.ts";
23
23
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gheim",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "PII round-trip for LLM APIs: anonymize before the request, de-anonymize the stream on the way back.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Joel Barmettler <joel.barmettler@gmail.com>",