gheim 0.1.1 → 0.1.2

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/dist/esm/index.js CHANGED
@@ -1,5 +1,10 @@
1
- import { createRequire } from "node:module";
2
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
3
8
 
4
9
  // src/core/sentinels.ts
5
10
  var LABEL_TO_TAG = {
@@ -37,98 +42,6 @@ function isPossibleSentinelPrefix(buffer) {
37
42
  return PARTIAL_SENTINEL_RE.test(buffer);
38
43
  }
39
44
 
40
- // src/normalizers.ts
41
- import { createRequire as createRequire2 } from "node:module";
42
- var _require = null;
43
- var _requireResolved = false;
44
- function _getRequire() {
45
- if (_requireResolved)
46
- return _require;
47
- _requireResolved = true;
48
- try {
49
- _require = createRequire2(import.meta.url);
50
- } catch {
51
- _require = null;
52
- }
53
- return _require;
54
- }
55
- function loadSync(name, errMsg) {
56
- const req = _getRequire();
57
- if (!req)
58
- throw new Error(errMsg);
59
- try {
60
- return req(name);
61
- } catch {
62
- throw new Error(errMsg);
63
- }
64
- }
65
- function loadPhoneLib() {
66
- return loadSync("libphonenumber-js", "the e164 normalizer requires libphonenumber-js. " + "Install with: bun add libphonenumber-js");
67
- }
68
- function e164(opts = {}) {
69
- const lib = loadPhoneLib();
70
- return (s) => {
71
- try {
72
- const parsed = lib.parsePhoneNumberFromString(s, opts.region);
73
- if (!parsed || !parsed.isValid())
74
- return null;
75
- return parsed.format("E.164");
76
- } catch {
77
- return null;
78
- }
79
- };
80
- }
81
- function loadChrono(opts) {
82
- const mod = loadSync("chrono-node", "the isoDate normalizer requires chrono-node. " + "Install with: bun add chrono-node");
83
- if (opts.locale) {
84
- const localeParser = mod[opts.locale];
85
- if (localeParser)
86
- return localeParser;
87
- }
88
- return mod;
89
- }
90
- function isoDate(opts = {}) {
91
- const parser = loadChrono(opts);
92
- return (s) => {
93
- const d = parser.parseDate(s);
94
- if (!d)
95
- return null;
96
- return d.toISOString().slice(0, 10);
97
- };
98
- }
99
- function germanTransliteration() {
100
- const table = {
101
- ä: "ae",
102
- Ä: "ae",
103
- ö: "oe",
104
- Ö: "oe",
105
- ü: "ue",
106
- Ü: "ue",
107
- ß: "ss"
108
- };
109
- return (s) => {
110
- let out = "";
111
- for (const ch of s.normalize("NFKC")) {
112
- out += table[ch] ?? ch;
113
- }
114
- return out.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
115
- };
116
- }
117
- var BUILTINS = {
118
- e164: () => e164(),
119
- iso_date: () => isoDate(),
120
- german: () => germanTransliteration()
121
- };
122
- function resolveNormalizer(spec) {
123
- if (typeof spec === "function")
124
- return spec;
125
- const factory = BUILTINS[spec];
126
- if (!factory) {
127
- throw new Error(`unknown built-in normalizer: ${JSON.stringify(spec)}. ` + `Available: ${Object.keys(BUILTINS).join(", ")}, or pass a callable.`);
128
- }
129
- return factory();
130
- }
131
-
132
45
  // src/core/session.ts
133
46
  function normalizeSurface(s) {
134
47
  return s.normalize("NFKC").toLowerCase().split(/\s+/).filter(Boolean).join(" ");
@@ -141,8 +54,8 @@ class Session {
141
54
  resolvedNormalizers = {};
142
55
  constructor(opts = {}) {
143
56
  if (opts.normalizers) {
144
- for (const [label, spec] of Object.entries(opts.normalizers)) {
145
- this.resolvedNormalizers[label] = resolveNormalizer(spec);
57
+ for (const [label, fn] of Object.entries(opts.normalizers)) {
58
+ this.resolvedNormalizers[label] = fn;
146
59
  }
147
60
  }
148
61
  }
@@ -166,8 +79,11 @@ class Session {
166
79
  return sentinel;
167
80
  }
168
81
  applySpans(text, spans) {
82
+ return this.applySpansDetailed(text, spans).redacted;
83
+ }
84
+ applySpansDetailed(text, spans) {
169
85
  if (spans.length === 0)
170
- return text;
86
+ return { redacted: text, merged: [] };
171
87
  const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
172
88
  const merged = mergeAdjacent(text, sorted);
173
89
  const out = [];
@@ -183,7 +99,7 @@ class Session {
183
99
  cursor = end;
184
100
  }
185
101
  out.push(text.slice(cursor));
186
- return out.join("");
102
+ return { redacted: out.join(""), merged };
187
103
  }
188
104
  restore(text) {
189
105
  if (Object.keys(this.mapping).length === 0)
@@ -381,8 +297,10 @@ class LocalDetector {
381
297
  chunkTokens;
382
298
  chunkOverlapTokens;
383
299
  batchSize;
300
+ actualDevice = null;
384
301
  _pipeline = null;
385
302
  _loadPromise = null;
303
+ _onProgress;
386
304
  constructor(opts = {}) {
387
305
  this.model = opts.model ?? DEFAULT_MODEL2;
388
306
  this.device = opts.device ?? "auto";
@@ -391,11 +309,13 @@ class LocalDetector {
391
309
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
392
310
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
393
311
  this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
312
+ this._onProgress = opts.onProgress;
394
313
  if (this.chunkOverlapTokens >= this.chunkTokens) {
395
314
  throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than chunkTokens (${this.chunkTokens})`);
396
315
  }
397
316
  if (opts.pipelineInstance) {
398
317
  this._pipeline = opts.pipelineInstance;
318
+ this.actualDevice = this.device === "auto" ? null : this.device;
399
319
  }
400
320
  }
401
321
  async load() {
@@ -404,24 +324,70 @@ class LocalDetector {
404
324
  if (this._loadPromise)
405
325
  return this._loadPromise;
406
326
  this._loadPromise = (async () => {
407
- let mod;
408
- try {
409
- mod = await import("@huggingface/transformers");
410
- } catch (e) {
411
- throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
327
+ const mod = await loadTransformersJs();
328
+ const sequence = await this._planDeviceSequence();
329
+ let lastErr;
330
+ for (let i = 0;i < sequence.length; i++) {
331
+ const dev = sequence[i] ?? "wasm";
332
+ try {
333
+ const pipe = await mod.pipeline("token-classification", this.model, {
334
+ device: dev,
335
+ dtype: this.dtype,
336
+ progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
337
+ });
338
+ this._pipeline = pipe;
339
+ this.actualDevice = dev;
340
+ return pipe;
341
+ } catch (e) {
342
+ lastErr = e;
343
+ if (i < sequence.length - 1)
344
+ continue;
345
+ throw e;
346
+ }
412
347
  }
413
- const pipe = await mod.pipeline("token-classification", this.model, {
414
- device: this.device,
415
- dtype: this.dtype
416
- });
417
- this._pipeline = pipe;
418
- return pipe;
348
+ throw lastErr ?? new Error("LocalDetector.load(): no candidate backends");
419
349
  })();
420
350
  return this._loadPromise;
421
351
  }
352
+ async _planDeviceSequence() {
353
+ if (this.device !== "auto")
354
+ return [this.device];
355
+ const seq = [];
356
+ const nav = typeof navigator !== "undefined" ? navigator : null;
357
+ if (nav?.gpu?.requestAdapter) {
358
+ try {
359
+ const adapter = await nav.gpu.requestAdapter();
360
+ if (adapter)
361
+ seq.push("webgpu");
362
+ } catch {}
363
+ }
364
+ seq.push("wasm");
365
+ return seq;
366
+ }
367
+ _emitProgress(raw, device) {
368
+ if (!this._onProgress)
369
+ return;
370
+ const event = {
371
+ phase: raw.status ?? "progress",
372
+ device
373
+ };
374
+ if (raw.file)
375
+ event.file = raw.file;
376
+ if (raw.status === "progress") {
377
+ if (raw.loaded != null)
378
+ event.loadedBytes = raw.loaded;
379
+ if (raw.total != null)
380
+ event.totalBytes = raw.total;
381
+ if (raw.progress != null) {
382
+ const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
383
+ event.fraction = Math.max(0, Math.min(1, norm));
384
+ }
385
+ }
386
+ this._onProgress(event);
387
+ }
422
388
  async detect(text, opts) {
423
389
  if (opts?.model && opts.model !== this.model) {
424
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
390
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
425
391
  }
426
392
  if (!text)
427
393
  return [];
@@ -430,7 +396,7 @@ class LocalDetector {
430
396
  }
431
397
  async detectBatch(texts, opts) {
432
398
  if (opts?.model && opts.model !== this.model) {
433
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
399
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
434
400
  }
435
401
  if (texts.length === 0)
436
402
  return [];
@@ -591,6 +557,13 @@ function chunkByChars(text, chunkChars, overlapChars) {
591
557
  function normalizeWord(word) {
592
558
  return word.replace(/##/g, "").trim();
593
559
  }
560
+ async function loadTransformersJs() {
561
+ try {
562
+ return await import("@huggingface/transformers");
563
+ } catch {
564
+ throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
565
+ }
566
+ }
594
567
  // src/detectors/index.ts
595
568
  function defaultDetector() {
596
569
  const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
@@ -645,10 +618,11 @@ async function* deanonymizeStream(chunks, session) {
645
618
  }
646
619
 
647
620
  // src/index.ts
648
- var VERSION = "0.1.0";
621
+ var VERSION = "0.1.2";
649
622
  export {
650
623
  renderSentinel,
651
624
  parseSentinel,
625
+ mergeAdjacent,
652
626
  labelTag,
653
627
  isPossibleSentinelPrefix,
654
628
  defaultDetector,
@@ -1,5 +1,10 @@
1
- import { createRequire } from "node:module";
2
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined")
5
+ return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
3
8
 
4
9
  // src/core/sentinels.ts
5
10
  var LABEL_TO_TAG = {
@@ -228,8 +233,10 @@ class LocalDetector {
228
233
  chunkTokens;
229
234
  chunkOverlapTokens;
230
235
  batchSize;
236
+ actualDevice = null;
231
237
  _pipeline = null;
232
238
  _loadPromise = null;
239
+ _onProgress;
233
240
  constructor(opts = {}) {
234
241
  this.model = opts.model ?? DEFAULT_MODEL2;
235
242
  this.device = opts.device ?? "auto";
@@ -238,11 +245,13 @@ class LocalDetector {
238
245
  this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
239
246
  this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
240
247
  this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
248
+ this._onProgress = opts.onProgress;
241
249
  if (this.chunkOverlapTokens >= this.chunkTokens) {
242
250
  throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than chunkTokens (${this.chunkTokens})`);
243
251
  }
244
252
  if (opts.pipelineInstance) {
245
253
  this._pipeline = opts.pipelineInstance;
254
+ this.actualDevice = this.device === "auto" ? null : this.device;
246
255
  }
247
256
  }
248
257
  async load() {
@@ -251,24 +260,70 @@ class LocalDetector {
251
260
  if (this._loadPromise)
252
261
  return this._loadPromise;
253
262
  this._loadPromise = (async () => {
254
- let mod;
255
- try {
256
- mod = await import("@huggingface/transformers");
257
- } catch (e) {
258
- throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
263
+ const mod = await loadTransformersJs();
264
+ const sequence = await this._planDeviceSequence();
265
+ let lastErr;
266
+ for (let i = 0;i < sequence.length; i++) {
267
+ const dev = sequence[i] ?? "wasm";
268
+ try {
269
+ const pipe = await mod.pipeline("token-classification", this.model, {
270
+ device: dev,
271
+ dtype: this.dtype,
272
+ progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, dev) : undefined
273
+ });
274
+ this._pipeline = pipe;
275
+ this.actualDevice = dev;
276
+ return pipe;
277
+ } catch (e) {
278
+ lastErr = e;
279
+ if (i < sequence.length - 1)
280
+ continue;
281
+ throw e;
282
+ }
259
283
  }
260
- const pipe = await mod.pipeline("token-classification", this.model, {
261
- device: this.device,
262
- dtype: this.dtype
263
- });
264
- this._pipeline = pipe;
265
- return pipe;
284
+ throw lastErr ?? new Error("LocalDetector.load(): no candidate backends");
266
285
  })();
267
286
  return this._loadPromise;
268
287
  }
288
+ async _planDeviceSequence() {
289
+ if (this.device !== "auto")
290
+ return [this.device];
291
+ const seq = [];
292
+ const nav = typeof navigator !== "undefined" ? navigator : null;
293
+ if (nav?.gpu?.requestAdapter) {
294
+ try {
295
+ const adapter = await nav.gpu.requestAdapter();
296
+ if (adapter)
297
+ seq.push("webgpu");
298
+ } catch {}
299
+ }
300
+ seq.push("wasm");
301
+ return seq;
302
+ }
303
+ _emitProgress(raw, device) {
304
+ if (!this._onProgress)
305
+ return;
306
+ const event = {
307
+ phase: raw.status ?? "progress",
308
+ device
309
+ };
310
+ if (raw.file)
311
+ event.file = raw.file;
312
+ if (raw.status === "progress") {
313
+ if (raw.loaded != null)
314
+ event.loadedBytes = raw.loaded;
315
+ if (raw.total != null)
316
+ event.totalBytes = raw.total;
317
+ if (raw.progress != null) {
318
+ const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
319
+ event.fraction = Math.max(0, Math.min(1, norm));
320
+ }
321
+ }
322
+ this._onProgress(event);
323
+ }
269
324
  async detect(text, opts) {
270
325
  if (opts?.model && opts.model !== this.model) {
271
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
326
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
272
327
  }
273
328
  if (!text)
274
329
  return [];
@@ -277,7 +332,7 @@ class LocalDetector {
277
332
  }
278
333
  async detectBatch(texts, opts) {
279
334
  if (opts?.model && opts.model !== this.model) {
280
- throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
335
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
281
336
  }
282
337
  if (texts.length === 0)
283
338
  return [];
@@ -438,102 +493,17 @@ function chunkByChars(text, chunkChars, overlapChars) {
438
493
  function normalizeWord(word) {
439
494
  return word.replace(/##/g, "").trim();
440
495
  }
441
- // src/detectors/index.ts
442
- function defaultDetector() {
443
- const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
444
- return hasKey ? new RemoteDetector : new LocalDetector;
445
- }
446
-
447
- // src/normalizers.ts
448
- import { createRequire as createRequire2 } from "node:module";
449
- var _require = null;
450
- var _requireResolved = false;
451
- function _getRequire() {
452
- if (_requireResolved)
453
- return _require;
454
- _requireResolved = true;
455
- try {
456
- _require = createRequire2(import.meta.url);
457
- } catch {
458
- _require = null;
459
- }
460
- return _require;
461
- }
462
- function loadSync(name, errMsg) {
463
- const req = _getRequire();
464
- if (!req)
465
- throw new Error(errMsg);
496
+ async function loadTransformersJs() {
466
497
  try {
467
- return req(name);
498
+ return await import("@huggingface/transformers");
468
499
  } catch {
469
- throw new Error(errMsg);
500
+ throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
470
501
  }
471
502
  }
472
- function loadPhoneLib() {
473
- return loadSync("libphonenumber-js", "the e164 normalizer requires libphonenumber-js. " + "Install with: bun add libphonenumber-js");
474
- }
475
- function e164(opts = {}) {
476
- const lib = loadPhoneLib();
477
- return (s) => {
478
- try {
479
- const parsed = lib.parsePhoneNumberFromString(s, opts.region);
480
- if (!parsed || !parsed.isValid())
481
- return null;
482
- return parsed.format("E.164");
483
- } catch {
484
- return null;
485
- }
486
- };
487
- }
488
- function loadChrono(opts) {
489
- const mod = loadSync("chrono-node", "the isoDate normalizer requires chrono-node. " + "Install with: bun add chrono-node");
490
- if (opts.locale) {
491
- const localeParser = mod[opts.locale];
492
- if (localeParser)
493
- return localeParser;
494
- }
495
- return mod;
496
- }
497
- function isoDate(opts = {}) {
498
- const parser = loadChrono(opts);
499
- return (s) => {
500
- const d = parser.parseDate(s);
501
- if (!d)
502
- return null;
503
- return d.toISOString().slice(0, 10);
504
- };
505
- }
506
- function germanTransliteration() {
507
- const table = {
508
- ä: "ae",
509
- Ä: "ae",
510
- ö: "oe",
511
- Ö: "oe",
512
- ü: "ue",
513
- Ü: "ue",
514
- ß: "ss"
515
- };
516
- return (s) => {
517
- let out = "";
518
- for (const ch of s.normalize("NFKC")) {
519
- out += table[ch] ?? ch;
520
- }
521
- return out.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
522
- };
523
- }
524
- var BUILTINS = {
525
- e164: () => e164(),
526
- iso_date: () => isoDate(),
527
- german: () => germanTransliteration()
528
- };
529
- function resolveNormalizer(spec) {
530
- if (typeof spec === "function")
531
- return spec;
532
- const factory = BUILTINS[spec];
533
- if (!factory) {
534
- throw new Error(`unknown built-in normalizer: ${JSON.stringify(spec)}. ` + `Available: ${Object.keys(BUILTINS).join(", ")}, or pass a callable.`);
535
- }
536
- return factory();
503
+ // src/detectors/index.ts
504
+ function defaultDetector() {
505
+ const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
506
+ return hasKey ? new RemoteDetector : new LocalDetector;
537
507
  }
538
508
 
539
509
  // src/core/session.ts
@@ -548,8 +518,8 @@ class Session {
548
518
  resolvedNormalizers = {};
549
519
  constructor(opts = {}) {
550
520
  if (opts.normalizers) {
551
- for (const [label, spec] of Object.entries(opts.normalizers)) {
552
- this.resolvedNormalizers[label] = resolveNormalizer(spec);
521
+ for (const [label, fn] of Object.entries(opts.normalizers)) {
522
+ this.resolvedNormalizers[label] = fn;
553
523
  }
554
524
  }
555
525
  }
@@ -573,8 +543,11 @@ class Session {
573
543
  return sentinel;
574
544
  }
575
545
  applySpans(text, spans) {
546
+ return this.applySpansDetailed(text, spans).redacted;
547
+ }
548
+ applySpansDetailed(text, spans) {
576
549
  if (spans.length === 0)
577
- return text;
550
+ return { redacted: text, merged: [] };
578
551
  const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
579
552
  const merged = mergeAdjacent(text, sorted);
580
553
  const out = [];
@@ -590,7 +563,7 @@ class Session {
590
563
  cursor = end;
591
564
  }
592
565
  out.push(text.slice(cursor));
593
- return out.join("");
566
+ return { redacted: out.join(""), merged };
594
567
  }
595
568
  restore(text) {
596
569
  if (Object.keys(this.mapping).length === 0)
@@ -823,6 +796,16 @@ function restoreChatResponse(response, session) {
823
796
  if (msg && typeof msg.content === "string") {
824
797
  msg.content = session.restore(msg.content);
825
798
  }
799
+ if (msg && typeof msg.reasoning === "string") {
800
+ msg.reasoning = session.restore(msg.reasoning);
801
+ }
802
+ if (msg && Array.isArray(msg.reasoning_details)) {
803
+ for (const r of msg.reasoning_details) {
804
+ if (r && typeof r.text === "string") {
805
+ r.text = session.restore(r.text);
806
+ }
807
+ }
808
+ }
826
809
  for (const tc of msg?.tool_calls ?? []) {
827
810
  const fn = tc.function;
828
811
  if (fn && typeof fn.arguments === "string") {
@@ -837,6 +820,16 @@ function patchChatChunk(chunk, buf) {
837
820
  if (typeof delta.content === "string") {
838
821
  delta.content = buf.feed(delta.content);
839
822
  }
823
+ if (typeof delta.reasoning === "string") {
824
+ delta.reasoning = buf.feed(delta.reasoning);
825
+ }
826
+ if (Array.isArray(delta.reasoning_details)) {
827
+ for (const r of delta.reasoning_details) {
828
+ if (r && typeof r.text === "string") {
829
+ r.text = buf.feed(r.text);
830
+ }
831
+ }
832
+ }
840
833
  }
841
834
  }
842
835
  function makeChatTailChunk(template, content) {
@@ -1163,9 +1156,20 @@ class OpenAI {
1163
1156
  gheimStrict;
1164
1157
  _clientPromise;
1165
1158
  constructor(opts = {}) {
1166
- const detector = opts.gheimDetector ?? defaultDetector();
1167
- this.gheimStrict = opts.gheimStrict ?? true;
1168
- this._clientPromise = opts.openaiClient ? Promise.resolve(opts.openaiClient) : loadOpenAI(opts.clientOptions);
1159
+ const {
1160
+ gheimDetector,
1161
+ gheimStrict,
1162
+ openaiClient,
1163
+ clientOptions,
1164
+ ...passthrough
1165
+ } = opts;
1166
+ const detector = gheimDetector ?? defaultDetector();
1167
+ this.gheimStrict = gheimStrict ?? true;
1168
+ const innerOpts = {
1169
+ ...clientOptions ?? {},
1170
+ ...passthrough
1171
+ };
1172
+ this._clientPromise = openaiClient ? Promise.resolve(openaiClient) : loadOpenAI(innerOpts);
1169
1173
  this._clientPromise.then((c) => {
1170
1174
  this.raw = c;
1171
1175
  });
@@ -1,15 +1,24 @@
1
1
  /** Session state — mirrors gheim-py's Session. */
2
2
  import type { Span } from "../detectors/base.ts";
3
- import type { Normalizer } from "../normalizers.ts";
3
+ /** A surface canonicaliser. Maps a raw surface to a canonical key, or
4
+ * null to fall back to the default NFKC + lowercase key. Re-declared
5
+ * locally so this module doesn't pull in `gheim/normalizers` (which
6
+ * uses Node's `createRequire` and breaks browser bundlers). */
7
+ export type Normalizer = (s: string) => string | null;
4
8
  /** Constructor options for Session. */
5
9
  export interface SessionOptions {
6
10
  /**
7
11
  * Optional per-label canonical-form normalizers. Maps a label name
8
- * (e.g. "private_phone") to either a built-in name ("e164" /
9
- * "iso_date") or a callable Normalizer. Surfaces that fail to parse
10
- * fall back to the default NFKC+lowercase key. See ./normalizers.ts.
12
+ * (e.g. "private_phone") to a callable Normalizer. Surfaces that fail
13
+ * to parse fall back to the default NFKC+lowercase key.
14
+ *
15
+ * Built-in factories (e164, isoDate, germanTransliteration) live in
16
+ * the `gheim/normalizers` subpath — they pull in optional peer deps
17
+ * (`libphonenumber-js`, `chrono-node`) so they're not loaded by
18
+ * default to keep the browser bundle small and avoid Node-only
19
+ * imports leaking into the main entry.
11
20
  */
12
- normalizers?: Record<string, string | Normalizer>;
21
+ normalizers?: Record<string, Normalizer>;
13
22
  }
14
23
  export declare class Session {
15
24
  /** Sentinel string ("<PERSON_1>") → original surface form. */
@@ -19,7 +28,19 @@ export declare class Session {
19
28
  private resolvedNormalizers;
20
29
  constructor(opts?: SessionOptions);
21
30
  allocate(label: string, surface: string): string;
22
- applySpans(text: string, spans: Span[]): string;
31
+ applySpans(text: string, spans: readonly (Span | MergedSpan)[]): string;
32
+ /**
33
+ * Like {@link applySpans} but also returns the post-merge span list. UI
34
+ * consumers (segment rendering, bar overlays) typically need both the
35
+ * redacted text and the merged spans so they can highlight the redacted
36
+ * runs at their original character positions; without this method they'd
37
+ * have to call {@link mergeAdjacent} again on the same input. Pure
38
+ * convenience — no extra cost over `applySpans`.
39
+ */
40
+ applySpansDetailed(text: string, spans: readonly (Span | MergedSpan)[]): {
41
+ redacted: string;
42
+ merged: MergedSpan[];
43
+ };
23
44
  restore(text: string): string;
24
45
  toJSON(): {
25
46
  mapping: Record<string, string>;
@@ -28,4 +49,19 @@ export declare class Session {
28
49
  mapping: Record<string, string>;
29
50
  }, opts?: SessionOptions): Session;
30
51
  }
52
+ /** A span after merge: same shape as input, but no `text`/`score` carried. */
53
+ export interface MergedSpan {
54
+ start: number;
55
+ end: number;
56
+ label: string;
57
+ }
58
+ /** Merge runs of adjacent same-label spans (BPE-split surface fix).
59
+ *
60
+ * Two same-label spans separated by ≤ MERGE_GAP characters of pure
61
+ * whitespace (or directly touching) collapse into one. Models tokenize
62
+ * a phone like "+41 44 268 12 34" into many single-space-separated
63
+ * pieces with the same label; merging recovers the original entity so
64
+ * the demo and SDK both see one PHONE span instead of five.
65
+ */
66
+ export declare function mergeAdjacent(text: string, spans: readonly (Span | MergedSpan)[]): MergedSpan[];
31
67
  //# sourceMappingURL=session.d.ts.map
@@ -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;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAmBpD,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC,CAAC;CACnD;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,IAAI,EAAE,GAAG,MAAM;IAoB/C,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"}
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;IAoB7C,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"}