gheim 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +208 -0
  3. package/dist/cjs/index.cjs +707 -0
  4. package/dist/cjs/openai/index.cjs +1291 -0
  5. package/dist/esm/index.js +668 -0
  6. package/dist/esm/openai/index.js +1252 -0
  7. package/dist/types/core/sentinels.d.ts +24 -0
  8. package/dist/types/core/sentinels.d.ts.map +1 -0
  9. package/dist/types/core/session.d.ts +31 -0
  10. package/dist/types/core/session.d.ts.map +1 -0
  11. package/dist/types/core/stream.d.ts +9 -0
  12. package/dist/types/core/stream.d.ts.map +1 -0
  13. package/dist/types/detectors/base.d.ts +28 -0
  14. package/dist/types/detectors/base.d.ts.map +1 -0
  15. package/dist/types/detectors/index.d.ts +9 -0
  16. package/dist/types/detectors/index.d.ts.map +1 -0
  17. package/dist/types/detectors/local.d.ts +68 -0
  18. package/dist/types/detectors/local.d.ts.map +1 -0
  19. package/dist/types/detectors/remote.d.ts +24 -0
  20. package/dist/types/detectors/remote.d.ts.map +1 -0
  21. package/dist/types/index.d.ts +20 -0
  22. package/dist/types/index.d.ts.map +1 -0
  23. package/dist/types/normalizers.d.ts +89 -0
  24. package/dist/types/normalizers.d.ts.map +1 -0
  25. package/dist/types/openai/_anonymize_only.d.ts +25 -0
  26. package/dist/types/openai/_anonymize_only.d.ts.map +1 -0
  27. package/dist/types/openai/_audio.d.ts +41 -0
  28. package/dist/types/openai/_audio.d.ts.map +1 -0
  29. package/dist/types/openai/_base.d.ts +31 -0
  30. package/dist/types/openai/_base.d.ts.map +1 -0
  31. package/dist/types/openai/_chat.d.ts +16 -0
  32. package/dist/types/openai/_chat.d.ts.map +1 -0
  33. package/dist/types/openai/_client.d.ts +36 -0
  34. package/dist/types/openai/_client.d.ts.map +1 -0
  35. package/dist/types/openai/_completions.d.ts +11 -0
  36. package/dist/types/openai/_completions.d.ts.map +1 -0
  37. package/dist/types/openai/_responses.d.ts +13 -0
  38. package/dist/types/openai/_responses.d.ts.map +1 -0
  39. package/dist/types/openai/_strict.d.ts +15 -0
  40. package/dist/types/openai/_strict.d.ts.map +1 -0
  41. package/dist/types/openai/index.d.ts +25 -0
  42. package/dist/types/openai/index.d.ts.map +1 -0
  43. package/dist/types/plain.d.ts +34 -0
  44. package/dist/types/plain.d.ts.map +1 -0
  45. package/package.json +69 -0
@@ -0,0 +1,707 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
8
+ var __toCommonJS = (from) => {
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
+ if (entry)
11
+ return entry;
12
+ entry = __defProp({}, "__esModule", { value: true });
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ __moduleCache.set(from, entry);
22
+ return entry;
23
+ };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
29
+ var __export = (target, all) => {
30
+ for (var name in all)
31
+ __defProp(target, name, {
32
+ get: all[name],
33
+ enumerable: true,
34
+ configurable: true,
35
+ set: __exportSetter.bind(all, name)
36
+ });
37
+ };
38
+
39
+ // src/index.ts
40
+ var exports_src = {};
41
+ __export(exports_src, {
42
+ renderSentinel: () => renderSentinel,
43
+ parseSentinel: () => parseSentinel,
44
+ labelTag: () => labelTag,
45
+ isPossibleSentinelPrefix: () => isPossibleSentinelPrefix,
46
+ defaultDetector: () => defaultDetector,
47
+ deanonymizeText: () => deanonymizeText,
48
+ deanonymizeStream: () => deanonymizeStream,
49
+ anonymizeText: () => anonymizeText,
50
+ anonymizeMessages: () => anonymizeMessages,
51
+ VERSION: () => VERSION,
52
+ StreamDeanonymizer: () => StreamDeanonymizer,
53
+ Session: () => Session,
54
+ SENTINEL_RE: () => SENTINEL_RE,
55
+ RemoteDetector: () => RemoteDetector,
56
+ REMOTE_DEFAULT_MODEL: () => DEFAULT_MODEL,
57
+ LocalDetector: () => LocalDetector,
58
+ LABEL_TO_TAG: () => LABEL_TO_TAG,
59
+ DEFAULT_BASE_URL: () => DEFAULT_BASE_URL
60
+ });
61
+ module.exports = __toCommonJS(exports_src);
62
+
63
+ // src/core/sentinels.ts
64
+ var LABEL_TO_TAG = {
65
+ private_person: "PERSON",
66
+ private_email: "EMAIL",
67
+ private_phone: "PHONE",
68
+ private_address: "ADDRESS",
69
+ private_url: "URL",
70
+ private_date: "DATE",
71
+ account_number: "ACCOUNT",
72
+ secret: "SECRET"
73
+ };
74
+ var TAG_RE = /[A-Z][A-Z0-9_]*/;
75
+ var INDEX_RE = /\d+/;
76
+ var SENTINEL_RE = new RegExp(`<(?<tag>${TAG_RE.source})_(?<index>${INDEX_RE.source})>`, "g");
77
+ var SENTINEL_FULL_RE = new RegExp(`^<(?<tag>${TAG_RE.source})_(?<index>${INDEX_RE.source})>$`);
78
+ var PARTIAL_SENTINEL_RE = new RegExp(`^<(${TAG_RE.source})?(_(${INDEX_RE.source})?)?>?$`);
79
+ function labelTag(label) {
80
+ return LABEL_TO_TAG[label] ?? label.toUpperCase();
81
+ }
82
+ function renderSentinel(s) {
83
+ return `<${s.tag}_${s.index}>`;
84
+ }
85
+ function parseSentinel(s) {
86
+ const m = SENTINEL_FULL_RE.exec(s);
87
+ if (!m || !m.groups)
88
+ return null;
89
+ return { tag: m.groups.tag, index: Number.parseInt(m.groups.index, 10) };
90
+ }
91
+ function isPossibleSentinelPrefix(buffer) {
92
+ if (!buffer || buffer[0] !== "<")
93
+ return false;
94
+ if (buffer.endsWith(">"))
95
+ return false;
96
+ return PARTIAL_SENTINEL_RE.test(buffer);
97
+ }
98
+
99
+ // src/normalizers.ts
100
+ var import_node_module = require("node:module");
101
+ var _require = null;
102
+ var _requireResolved = false;
103
+ function _getRequire() {
104
+ if (_requireResolved)
105
+ return _require;
106
+ _requireResolved = true;
107
+ try {
108
+ _require = import_node_module.createRequire("file:///home/joelbarmettler/projects/gheim/packages/gheim-js/src/normalizers.ts");
109
+ } catch {
110
+ _require = null;
111
+ }
112
+ return _require;
113
+ }
114
+ function loadSync(name, errMsg) {
115
+ const req = _getRequire();
116
+ if (!req)
117
+ throw new Error(errMsg);
118
+ try {
119
+ return req(name);
120
+ } catch {
121
+ throw new Error(errMsg);
122
+ }
123
+ }
124
+ function loadPhoneLib() {
125
+ return loadSync("libphonenumber-js", "the e164 normalizer requires libphonenumber-js. " + "Install with: bun add libphonenumber-js");
126
+ }
127
+ function e164(opts = {}) {
128
+ const lib = loadPhoneLib();
129
+ return (s) => {
130
+ try {
131
+ const parsed = lib.parsePhoneNumberFromString(s, opts.region);
132
+ if (!parsed || !parsed.isValid())
133
+ return null;
134
+ return parsed.format("E.164");
135
+ } catch {
136
+ return null;
137
+ }
138
+ };
139
+ }
140
+ function loadChrono(opts) {
141
+ const mod = loadSync("chrono-node", "the isoDate normalizer requires chrono-node. " + "Install with: bun add chrono-node");
142
+ if (opts.locale) {
143
+ const localeParser = mod[opts.locale];
144
+ if (localeParser)
145
+ return localeParser;
146
+ }
147
+ return mod;
148
+ }
149
+ function isoDate(opts = {}) {
150
+ const parser = loadChrono(opts);
151
+ return (s) => {
152
+ const d = parser.parseDate(s);
153
+ if (!d)
154
+ return null;
155
+ return d.toISOString().slice(0, 10);
156
+ };
157
+ }
158
+ function germanTransliteration() {
159
+ const table = {
160
+ ä: "ae",
161
+ Ä: "ae",
162
+ ö: "oe",
163
+ Ö: "oe",
164
+ ü: "ue",
165
+ Ü: "ue",
166
+ ß: "ss"
167
+ };
168
+ return (s) => {
169
+ let out = "";
170
+ for (const ch of s.normalize("NFKC")) {
171
+ out += table[ch] ?? ch;
172
+ }
173
+ return out.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
174
+ };
175
+ }
176
+ var BUILTINS = {
177
+ e164: () => e164(),
178
+ iso_date: () => isoDate(),
179
+ german: () => germanTransliteration()
180
+ };
181
+ function resolveNormalizer(spec) {
182
+ if (typeof spec === "function")
183
+ return spec;
184
+ const factory = BUILTINS[spec];
185
+ if (!factory) {
186
+ throw new Error(`unknown built-in normalizer: ${JSON.stringify(spec)}. ` + `Available: ${Object.keys(BUILTINS).join(", ")}, or pass a callable.`);
187
+ }
188
+ return factory();
189
+ }
190
+
191
+ // src/core/session.ts
192
+ function normalizeSurface(s) {
193
+ return s.normalize("NFKC").toLowerCase().split(/\s+/).filter(Boolean).join(" ");
194
+ }
195
+
196
+ class Session {
197
+ mapping = {};
198
+ coalesce = new Map;
199
+ counters = new Map;
200
+ resolvedNormalizers = {};
201
+ constructor(opts = {}) {
202
+ if (opts.normalizers) {
203
+ for (const [label, spec] of Object.entries(opts.normalizers)) {
204
+ this.resolvedNormalizers[label] = resolveNormalizer(spec);
205
+ }
206
+ }
207
+ }
208
+ allocate(label, surface) {
209
+ const tag = labelTag(label);
210
+ const norm = this.resolvedNormalizers[label];
211
+ let normalized = null;
212
+ if (norm)
213
+ normalized = norm(surface);
214
+ if (normalized === null)
215
+ normalized = normalizeSurface(surface);
216
+ const key = `${tag}\x00${normalized}`;
217
+ const existing = this.coalesce.get(key);
218
+ if (existing)
219
+ return existing;
220
+ const next = (this.counters.get(tag) ?? 0) + 1;
221
+ this.counters.set(tag, next);
222
+ const sentinel = renderSentinel({ tag, index: next });
223
+ this.coalesce.set(key, sentinel);
224
+ this.mapping[sentinel] = surface;
225
+ return sentinel;
226
+ }
227
+ applySpans(text, spans) {
228
+ if (spans.length === 0)
229
+ return text;
230
+ const sorted = [...spans].sort((a, b) => a.start - b.start || b.end - b.start - (a.end - a.start));
231
+ const merged = mergeAdjacent(text, sorted);
232
+ const out = [];
233
+ let cursor = 0;
234
+ for (const { start, end, label } of merged) {
235
+ if (start < cursor)
236
+ continue;
237
+ if (start < 0 || end > text.length || start >= end)
238
+ continue;
239
+ const surface = text.slice(start, end);
240
+ out.push(text.slice(cursor, start));
241
+ out.push(this.allocate(label, surface));
242
+ cursor = end;
243
+ }
244
+ out.push(text.slice(cursor));
245
+ return out.join("");
246
+ }
247
+ restore(text) {
248
+ if (Object.keys(this.mapping).length === 0)
249
+ return text;
250
+ return text.replace(SENTINEL_RE, (match) => this.mapping[match] ?? match);
251
+ }
252
+ toJSON() {
253
+ return { mapping: { ...this.mapping } };
254
+ }
255
+ static fromJSON(raw, opts = {}) {
256
+ const s = new Session(opts);
257
+ s.mapping = { ...raw.mapping };
258
+ for (const [sentinel, surface] of Object.entries(s.mapping)) {
259
+ const parsed = parseSentinel(sentinel);
260
+ if (!parsed)
261
+ continue;
262
+ let normalized = null;
263
+ for (const [label, fn] of Object.entries(s.resolvedNormalizers)) {
264
+ if (labelTag(label) === parsed.tag) {
265
+ normalized = fn(surface);
266
+ if (normalized !== null)
267
+ break;
268
+ }
269
+ }
270
+ if (normalized === null)
271
+ normalized = normalizeSurface(surface);
272
+ s.coalesce.set(`${parsed.tag}\x00${normalized}`, sentinel);
273
+ s.counters.set(parsed.tag, Math.max(s.counters.get(parsed.tag) ?? 0, parsed.index));
274
+ }
275
+ return s;
276
+ }
277
+ }
278
+ var MERGE_GAP = 1;
279
+ function mergeAdjacent(text, spans) {
280
+ const out = [];
281
+ for (const span of spans) {
282
+ if (span.start < 0 || span.end > text.length || span.start >= span.end)
283
+ continue;
284
+ const last = out[out.length - 1];
285
+ if (!last) {
286
+ out.push({ start: span.start, end: span.end, label: span.label });
287
+ continue;
288
+ }
289
+ if (span.start < last.end) {
290
+ continue;
291
+ }
292
+ const gap = text.slice(last.end, span.start);
293
+ if (span.label === last.label && gap.length <= MERGE_GAP && gap.trim() === "") {
294
+ last.end = span.end;
295
+ } else {
296
+ out.push({ start: span.start, end: span.end, label: span.label });
297
+ }
298
+ }
299
+ return out;
300
+ }
301
+ // src/core/stream.ts
302
+ var SENTINEL_AT = /<([A-Z][A-Z0-9_]*)_(\d+)>/y;
303
+ var MAX_SENTINEL_LEN = 64;
304
+
305
+ class StreamDeanonymizer {
306
+ mapping;
307
+ hold = "";
308
+ constructor(mapping) {
309
+ this.mapping = mapping;
310
+ }
311
+ feed(chunk) {
312
+ if (!chunk)
313
+ return "";
314
+ const buffer = this.hold + chunk;
315
+ const out = [];
316
+ let i = 0;
317
+ const n = buffer.length;
318
+ while (i < n) {
319
+ if (buffer[i] !== "<") {
320
+ const lt = buffer.indexOf("<", i);
321
+ if (lt === -1) {
322
+ out.push(buffer.slice(i));
323
+ i = n;
324
+ break;
325
+ }
326
+ out.push(buffer.slice(i, lt));
327
+ i = lt;
328
+ }
329
+ SENTINEL_AT.lastIndex = i;
330
+ const m = SENTINEL_AT.exec(buffer);
331
+ if (m && m.index === i) {
332
+ const sentinel = m[0];
333
+ out.push(this.mapping[sentinel] ?? sentinel);
334
+ i = SENTINEL_AT.lastIndex;
335
+ continue;
336
+ }
337
+ const tail = buffer.slice(i);
338
+ if (tail.length < MAX_SENTINEL_LEN && isPossibleSentinelPrefix(tail)) {
339
+ this.hold = tail;
340
+ return out.join("");
341
+ }
342
+ out.push("<");
343
+ i += 1;
344
+ }
345
+ this.hold = "";
346
+ return out.join("");
347
+ }
348
+ flush() {
349
+ const held = this.hold;
350
+ this.hold = "";
351
+ return held;
352
+ }
353
+ async* transform(chunks) {
354
+ for await (const chunk of chunks) {
355
+ const out = this.feed(chunk);
356
+ if (out)
357
+ yield out;
358
+ }
359
+ const tail = this.flush();
360
+ if (tail)
361
+ yield tail;
362
+ }
363
+ }
364
+ // src/detectors/base.ts
365
+ function trimSpan(text, start, end) {
366
+ while (start < end && /\s/.test(text[start] ?? ""))
367
+ start += 1;
368
+ while (end > start && /\s/.test(text[end - 1] ?? ""))
369
+ end -= 1;
370
+ if (start >= end)
371
+ return null;
372
+ return { start, end };
373
+ }
374
+
375
+ // src/detectors/remote.ts
376
+ var DEFAULT_BASE_URL = "https://api.gheim.ch";
377
+ var DEFAULT_MODEL = "openai/privacy-filter";
378
+
379
+ class RemoteDetector {
380
+ baseUrl;
381
+ apiKey;
382
+ model;
383
+ trimWhitespace;
384
+ fetchImpl;
385
+ constructor(opts = {}) {
386
+ const envBase = typeof process !== "undefined" ? process.env?.GHEIM_BASE_URL : undefined;
387
+ const envKey = typeof process !== "undefined" ? process.env?.GHEIM_API_KEY : undefined;
388
+ this.baseUrl = (opts.baseUrl ?? envBase ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
389
+ this.apiKey = opts.apiKey ?? envKey;
390
+ this.model = opts.model ?? DEFAULT_MODEL;
391
+ this.trimWhitespace = opts.trimWhitespace ?? true;
392
+ this.fetchImpl = opts.fetch ?? fetch;
393
+ }
394
+ async detect(text, opts) {
395
+ if (!text)
396
+ return [];
397
+ const headers = { "Content-Type": "application/json" };
398
+ if (this.apiKey)
399
+ headers.Authorization = `Bearer ${this.apiKey}`;
400
+ const resp = await this.fetchImpl(`${this.baseUrl}/v1/detect`, {
401
+ method: "POST",
402
+ headers,
403
+ body: JSON.stringify({ model: opts?.model ?? this.model, text })
404
+ });
405
+ if (!resp.ok) {
406
+ throw new Error(`gheim-server ${resp.status}: ${await resp.text()}`);
407
+ }
408
+ const data = await resp.json();
409
+ const out = [];
410
+ for (const item of data.spans ?? []) {
411
+ let { start, end } = item;
412
+ if (this.trimWhitespace) {
413
+ const trimmed = trimSpan(text, start, end);
414
+ if (!trimmed)
415
+ continue;
416
+ ({ start, end } = trimmed);
417
+ }
418
+ out.push({
419
+ label: item.label,
420
+ start,
421
+ end,
422
+ text: text.slice(start, end),
423
+ score: item.score
424
+ });
425
+ }
426
+ return out;
427
+ }
428
+ }
429
+ // src/detectors/local.ts
430
+ var DEFAULT_MODEL2 = "joelbarmettler/gheim-ch-560m";
431
+ var DEFAULT_CHUNK_TOKENS = 510;
432
+ var DEFAULT_OVERLAP_TOKENS = 32;
433
+ var DEFAULT_BATCH_SIZE = 8;
434
+
435
+ class LocalDetector {
436
+ model;
437
+ device;
438
+ dtype;
439
+ aggregationStrategy;
440
+ chunkTokens;
441
+ chunkOverlapTokens;
442
+ batchSize;
443
+ _pipeline = null;
444
+ _loadPromise = null;
445
+ constructor(opts = {}) {
446
+ this.model = opts.model ?? DEFAULT_MODEL2;
447
+ this.device = opts.device ?? "auto";
448
+ this.dtype = opts.dtype ?? "fp32";
449
+ this.aggregationStrategy = opts.aggregationStrategy ?? "simple";
450
+ this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS;
451
+ this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS;
452
+ this.batchSize = opts.batchSize ?? DEFAULT_BATCH_SIZE;
453
+ if (this.chunkOverlapTokens >= this.chunkTokens) {
454
+ throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than chunkTokens (${this.chunkTokens})`);
455
+ }
456
+ if (opts.pipelineInstance) {
457
+ this._pipeline = opts.pipelineInstance;
458
+ }
459
+ }
460
+ async load() {
461
+ if (this._pipeline)
462
+ return this._pipeline;
463
+ if (this._loadPromise)
464
+ return this._loadPromise;
465
+ this._loadPromise = (async () => {
466
+ let mod;
467
+ try {
468
+ mod = await import("@huggingface/transformers");
469
+ } catch (e) {
470
+ throw new Error("LocalDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
471
+ }
472
+ const pipe = await mod.pipeline("token-classification", this.model, {
473
+ device: this.device,
474
+ dtype: this.dtype
475
+ });
476
+ this._pipeline = pipe;
477
+ return pipe;
478
+ })();
479
+ return this._loadPromise;
480
+ }
481
+ async detect(text, opts) {
482
+ if (opts?.model && opts.model !== this.model) {
483
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
484
+ }
485
+ if (!text)
486
+ return [];
487
+ const results = await this.detectBatch([text]);
488
+ return results[0] ?? [];
489
+ }
490
+ async detectBatch(texts, opts) {
491
+ if (opts?.model && opts.model !== this.model) {
492
+ throw new Error(`LocalDetector was constructed with model='${this.model}'; per-call model override ('${opts.model}') not supported.`);
493
+ }
494
+ if (texts.length === 0)
495
+ return [];
496
+ const pipe = await this.load();
497
+ const tokenizer = pipe.tokenizer;
498
+ const plan = [];
499
+ for (let i = 0;i < texts.length; i++) {
500
+ const text = texts[i] ?? "";
501
+ if (!text)
502
+ continue;
503
+ const chunks = await this.chunkText(text, tokenizer);
504
+ for (const c of chunks)
505
+ plan.push({ inputIdx: i, charOffset: c.offset, text: c.text });
506
+ }
507
+ const out = texts.map(() => []);
508
+ if (plan.length === 0)
509
+ return out;
510
+ const batchInput = plan.map((p) => p.text);
511
+ const raw = await pipe(batchInput, {
512
+ aggregation_strategy: this.aggregationStrategy,
513
+ batch_size: this.batchSize
514
+ });
515
+ const seen = texts.map(() => new Set);
516
+ for (let p = 0;p < plan.length; p++) {
517
+ const planEntry = plan[p];
518
+ if (!planEntry)
519
+ continue;
520
+ const { inputIdx, charOffset, text: chunkText } = planEntry;
521
+ const fullText = texts[inputIdx] ?? "";
522
+ const inputSeen = seen[inputIdx];
523
+ const inputOut = out[inputIdx];
524
+ if (!inputSeen || !inputOut)
525
+ continue;
526
+ const items = raw[p] ?? [];
527
+ let cursor = 0;
528
+ for (const item of items) {
529
+ const label = String(item.entity_group ?? item.entity ?? "unknown");
530
+ const score = typeof item.score === "number" ? item.score : 1;
531
+ let localStart;
532
+ let localEnd;
533
+ if (item.start === undefined || item.end === undefined) {
534
+ const surface = normalizeWord(item.word ?? "");
535
+ if (!surface)
536
+ continue;
537
+ const idx = chunkText.indexOf(surface, cursor);
538
+ if (idx === -1)
539
+ continue;
540
+ localStart = idx;
541
+ localEnd = idx + surface.length;
542
+ cursor = localEnd;
543
+ } else {
544
+ localStart = item.start;
545
+ localEnd = item.end;
546
+ }
547
+ const start = localStart + charOffset;
548
+ const end = localEnd + charOffset;
549
+ const key = `${start}|${end}|${label}`;
550
+ if (inputSeen.has(key))
551
+ continue;
552
+ inputSeen.add(key);
553
+ inputOut.push({
554
+ label,
555
+ start,
556
+ end,
557
+ text: fullText.slice(start, end),
558
+ score
559
+ });
560
+ }
561
+ }
562
+ for (const spans of out) {
563
+ spans.sort((a, b) => a.start - b.start || a.end - b.end);
564
+ }
565
+ return out;
566
+ }
567
+ async chunkText(text, tokenizer) {
568
+ if (tokenizer) {
569
+ const tokenChunks = await chunkByTokens(text, tokenizer, this.chunkTokens, this.chunkOverlapTokens);
570
+ if (tokenChunks)
571
+ return tokenChunks;
572
+ }
573
+ return chunkByChars(text, 1500, 200);
574
+ }
575
+ }
576
+ async function chunkByTokens(text, tokenizer, chunkTokens, overlapTokens) {
577
+ let enc;
578
+ try {
579
+ enc = await tokenizer(text, {
580
+ return_offsets_mapping: true,
581
+ add_special_tokens: false,
582
+ truncation: false
583
+ });
584
+ } catch {
585
+ return null;
586
+ }
587
+ const offsets = readOffsetMapping(enc);
588
+ if (!offsets)
589
+ return null;
590
+ const real = offsets.filter(([a, b]) => !(a === 0 && b === 0));
591
+ if (real.length === 0)
592
+ return [{ offset: 0, text }];
593
+ if (real.length <= chunkTokens)
594
+ return [{ offset: 0, text }];
595
+ const stride = Math.max(1, chunkTokens - overlapTokens);
596
+ const out = [];
597
+ for (let s = 0;s < real.length; s += stride) {
598
+ const e = Math.min(s + chunkTokens, real.length);
599
+ const startPair = real[s];
600
+ const endPair = real[e - 1];
601
+ if (!startPair || !endPair)
602
+ break;
603
+ const charStart = startPair[0];
604
+ const charEnd = endPair[1];
605
+ out.push({ offset: charStart, text: text.slice(charStart, charEnd) });
606
+ if (e >= real.length)
607
+ break;
608
+ }
609
+ return out;
610
+ }
611
+ function readOffsetMapping(enc) {
612
+ const om = enc.offset_mapping;
613
+ if (!om)
614
+ return null;
615
+ if (typeof om.tolist === "function") {
616
+ const list = om.tolist();
617
+ if (!Array.isArray(list) || list.length === 0)
618
+ return [];
619
+ const inner = list[0];
620
+ if (Array.isArray(inner) && Array.isArray(inner[0])) {
621
+ return inner;
622
+ }
623
+ return list;
624
+ }
625
+ if (om.data && om.dims && om.dims.length >= 2) {
626
+ const dims = om.dims;
627
+ const pairs = dims[dims.length - 2] ?? 0;
628
+ const data = om.data;
629
+ const out = [];
630
+ for (let i = 0;i < pairs; i++) {
631
+ out.push([Number(data[i * 2]), Number(data[i * 2 + 1])]);
632
+ }
633
+ return out;
634
+ }
635
+ return null;
636
+ }
637
+ function chunkByChars(text, chunkChars, overlapChars) {
638
+ if (text.length <= chunkChars)
639
+ return [{ offset: 0, text }];
640
+ const stride = Math.max(1, chunkChars - overlapChars);
641
+ const out = [];
642
+ for (let s = 0;s < text.length; s += stride) {
643
+ const e = Math.min(s + chunkChars, text.length);
644
+ out.push({ offset: s, text: text.slice(s, e) });
645
+ if (e >= text.length)
646
+ break;
647
+ }
648
+ return out;
649
+ }
650
+ function normalizeWord(word) {
651
+ return word.replace(/##/g, "").trim();
652
+ }
653
+ // src/detectors/index.ts
654
+ function defaultDetector() {
655
+ const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
656
+ return hasKey ? new RemoteDetector : new LocalDetector;
657
+ }
658
+ // src/plain.ts
659
+ function resolve(session, detector) {
660
+ const det = detector ?? session.detector;
661
+ if (!det) {
662
+ throw new Error("No detector — pass one to anonymizeText or attach to the session.");
663
+ }
664
+ return det;
665
+ }
666
+ async function anonymizeText(text, session, opts) {
667
+ const det = resolve(session, opts?.detector);
668
+ const spans = await det.detect(text, { model: opts?.model });
669
+ return session.applySpans(text, spans);
670
+ }
671
+ function deanonymizeText(text, session) {
672
+ return session.restore(text);
673
+ }
674
+ async function anonymizeMessages(messages, session, opts) {
675
+ const det = resolve(session, opts?.detector);
676
+ const out = [];
677
+ for (const msg of messages) {
678
+ const content = msg.content;
679
+ if (typeof content === "string") {
680
+ const redacted = await anonymizeText(content, session, { detector: det, model: opts?.model });
681
+ out.push({ ...msg, content: redacted });
682
+ } else if (Array.isArray(content)) {
683
+ const parts = await Promise.all(content.map(async (p) => {
684
+ const part = p;
685
+ if (part && part.type === "text" && typeof part.text === "string") {
686
+ const redacted = await anonymizeText(part.text, session, {
687
+ detector: det,
688
+ model: opts?.model
689
+ });
690
+ return { ...part, text: redacted };
691
+ }
692
+ return p;
693
+ }));
694
+ out.push({ ...msg, content: parts });
695
+ } else {
696
+ out.push({ ...msg });
697
+ }
698
+ }
699
+ return out;
700
+ }
701
+ async function* deanonymizeStream(chunks, session) {
702
+ const buf = new StreamDeanonymizer(session.mapping);
703
+ yield* buf.transform(chunks);
704
+ }
705
+
706
+ // src/index.ts
707
+ var VERSION = "0.1.0";