gheim 0.1.4 → 0.1.6

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.
@@ -58,7 +58,8 @@ __export(exports_src, {
58
58
  LocalDetector: () => LocalDetector,
59
59
  LABEL_TO_TAG: () => LABEL_TO_TAG,
60
60
  DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
61
- CompositeDetector: () => CompositeDetector
61
+ CompositeDetector: () => CompositeDetector,
62
+ CalibratedDetector: () => CalibratedDetector
62
63
  });
63
64
  module.exports = __toCommonJS(exports_src);
64
65
 
@@ -156,8 +157,8 @@ class Session {
156
157
  }
157
158
  out.push(text.slice(cursor));
158
159
  let redacted = out.join("");
159
- redacted = recoverDuplicateOccurrences(redacted, this.mapping);
160
- return { redacted, merged };
160
+ const recovered = recoverDuplicateOccurrences(text, redacted, merged, this.mapping);
161
+ return { redacted: recovered.redacted, merged: recovered.merged };
161
162
  }
162
163
  restore(text) {
163
164
  if (Object.keys(this.mapping).length === 0)
@@ -216,14 +217,36 @@ function mergeAdjacent(text, spans) {
216
217
  function escapeRegex(s) {
217
218
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
218
219
  }
219
- 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);
220
226
  const entries = Object.entries(mapping).filter(([, surface]) => surface && surface.length > 0).sort((a, b) => b[1].length - a[1].length);
221
227
  let result = redacted;
228
+ const additional = [];
222
229
  for (const [sentinel, surface] of entries) {
223
- const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`, "gu");
224
- 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
+ }
225
245
  }
226
- 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 };
227
250
  }
228
251
  // src/core/stream.ts
229
252
  var SENTINEL_AT = /<([A-Z][A-Z0-9_]*)_(\d+)>/y;
@@ -842,10 +865,281 @@ class CompositeDetector {
842
865
  return out;
843
866
  }
844
867
  }
868
+ // src/detectors/calibrated.ts
869
+ var DEFAULT_MODEL3 = "joelbarmettler/gheim-ch-560m";
870
+ var DEFAULT_CHUNK_TOKENS2 = 510;
871
+ var DEFAULT_OVERLAP_TOKENS2 = 32;
872
+ function trimSpan2(text, start, end) {
873
+ while (start < end && /\s/.test(text[start] ?? ""))
874
+ start++;
875
+ while (end > start && /\s/.test(text[end - 1] ?? ""))
876
+ end--;
877
+ return [start, end];
878
+ }
879
+
880
+ class CalibratedDetector {
881
+ model;
882
+ oBias;
883
+ device;
884
+ dtype;
885
+ chunkTokens;
886
+ chunkOverlapTokens;
887
+ trimWhitespace;
888
+ actualDevice = null;
889
+ _model = null;
890
+ _tokenizer = null;
891
+ _id2label = {};
892
+ _oId = 0;
893
+ _loadPromise = null;
894
+ _onProgress;
895
+ constructor(opts = {}) {
896
+ this.model = opts.model ?? DEFAULT_MODEL3;
897
+ this.oBias = opts.oBias ?? 0.5;
898
+ this.device = opts.device ?? "auto";
899
+ this.dtype = opts.dtype ?? "fp32";
900
+ this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS2;
901
+ this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS2;
902
+ this.trimWhitespace = opts.trimWhitespace ?? true;
903
+ this._onProgress = opts.onProgress;
904
+ if (this.chunkOverlapTokens >= this.chunkTokens) {
905
+ throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than ` + `chunkTokens (${this.chunkTokens})`);
906
+ }
907
+ }
908
+ async load() {
909
+ if (this._model)
910
+ return;
911
+ if (this._loadPromise)
912
+ return this._loadPromise;
913
+ this._loadPromise = (async () => {
914
+ const mod = await loadTransformersJs2();
915
+ let preferredDevice = this.device;
916
+ if (this.device === "auto") {
917
+ const nav = typeof navigator !== "undefined" ? navigator : null;
918
+ preferredDevice = "wasm";
919
+ if (nav?.gpu?.requestAdapter) {
920
+ try {
921
+ const adapter = await nav.gpu.requestAdapter();
922
+ if (adapter)
923
+ preferredDevice = "webgpu";
924
+ } catch {}
925
+ }
926
+ }
927
+ const tokenizer = await mod.AutoTokenizer.from_pretrained(this.model);
928
+ const model = await mod.AutoModelForTokenClassification.from_pretrained(this.model, {
929
+ device: preferredDevice,
930
+ dtype: this.dtype,
931
+ progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, preferredDevice) : undefined
932
+ });
933
+ this._tokenizer = tokenizer;
934
+ this._model = model;
935
+ this._id2label = model.config.id2label;
936
+ const label2id = model.config.label2id;
937
+ this._oId = label2id?.["O"] ?? 0;
938
+ this.actualDevice = preferredDevice;
939
+ })();
940
+ return this._loadPromise;
941
+ }
942
+ async detect(text, opts) {
943
+ if (opts?.model && opts.model !== this.model) {
944
+ throw new Error(`CalibratedDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
945
+ }
946
+ if (!text)
947
+ return [];
948
+ await this.load();
949
+ return this._detectChunked(text);
950
+ }
951
+ async _detectChunked(text) {
952
+ if (text.length <= 1500) {
953
+ return await this._detectWindow(text, 0);
954
+ }
955
+ const stride = 1500 - 200;
956
+ const seen = new Set;
957
+ const out = [];
958
+ for (let s = 0;s < text.length; s += stride) {
959
+ const e = Math.min(s + 1500, text.length);
960
+ const sub = text.slice(s, e);
961
+ const subSpans = await this._detectWindow(sub, s);
962
+ for (const sp of subSpans) {
963
+ const key = `${sp.start}|${sp.end}|${sp.label}`;
964
+ if (seen.has(key))
965
+ continue;
966
+ seen.add(key);
967
+ out.push(sp);
968
+ }
969
+ if (e >= text.length)
970
+ break;
971
+ }
972
+ out.sort((a, b) => a.start - b.start || a.end - b.end);
973
+ return out;
974
+ }
975
+ async _detectWindow(text, charOffset) {
976
+ const tokenizer = this._tokenizer;
977
+ const model = this._model;
978
+ const enc = await tokenizer([text], {
979
+ padding: true,
980
+ truncation: true,
981
+ max_length: this.chunkTokens + 2
982
+ });
983
+ const inputs = {};
984
+ if (enc.input_ids)
985
+ inputs["input_ids"] = enc.input_ids;
986
+ if (enc.attention_mask)
987
+ inputs["attention_mask"] = enc.attention_mask;
988
+ const out = await model(inputs);
989
+ const logits = readLogits(out.logits);
990
+ if (this.oBias !== 0) {
991
+ for (let t = 0;t < logits.length; t++) {
992
+ const row = logits[t];
993
+ if (row === undefined)
994
+ continue;
995
+ const cur = row[this._oId];
996
+ if (cur !== undefined) {
997
+ row[this._oId] = cur - this.oBias;
998
+ }
999
+ }
1000
+ }
1001
+ const inputIds = readInputIds(enc.input_ids);
1002
+ const tokenLabels = [];
1003
+ for (let t = 0;t < logits.length; t++) {
1004
+ const row = logits[t];
1005
+ const id = inputIds[t];
1006
+ if (!row || id === undefined)
1007
+ continue;
1008
+ let maxIdx = 0;
1009
+ let maxVal = row[0];
1010
+ for (let i = 1;i < row.length; i++) {
1011
+ if (row[i] > maxVal) {
1012
+ maxVal = row[i];
1013
+ maxIdx = i;
1014
+ }
1015
+ }
1016
+ tokenLabels.push({ id, label: this._id2label[maxIdx] ?? "O" });
1017
+ }
1018
+ const spans = [];
1019
+ let cursor = 0;
1020
+ let groupIds = [];
1021
+ let groupCat = null;
1022
+ const flush = () => {
1023
+ if (groupCat === null || groupIds.length === 0)
1024
+ return;
1025
+ const surface = normalizeDecoded(tokenizer.decode(groupIds, { skip_special_tokens: true }));
1026
+ groupIds = [];
1027
+ const cat = groupCat;
1028
+ groupCat = null;
1029
+ if (!surface)
1030
+ return;
1031
+ const idx = text.indexOf(surface, cursor);
1032
+ if (idx === -1)
1033
+ return;
1034
+ let s = idx;
1035
+ let e = idx + surface.length;
1036
+ cursor = e;
1037
+ if (this.trimWhitespace)
1038
+ [s, e] = trimSpan2(text, s, e);
1039
+ if (s >= e)
1040
+ return;
1041
+ spans.push({
1042
+ start: s + charOffset,
1043
+ end: e + charOffset,
1044
+ label: cat,
1045
+ text: text.slice(s, e),
1046
+ score: 1
1047
+ });
1048
+ };
1049
+ for (const { id, label } of tokenLabels) {
1050
+ const cat = label === "O" ? null : label.split("-", 2)[1] ?? null;
1051
+ if (cat === groupCat && cat !== null) {
1052
+ groupIds.push(id);
1053
+ } else {
1054
+ flush();
1055
+ if (cat !== null) {
1056
+ groupIds.push(id);
1057
+ groupCat = cat;
1058
+ }
1059
+ }
1060
+ }
1061
+ flush();
1062
+ return spans;
1063
+ }
1064
+ _emitProgress(raw, device) {
1065
+ if (!this._onProgress)
1066
+ return;
1067
+ const event = {
1068
+ phase: raw.status ?? "progress",
1069
+ device
1070
+ };
1071
+ if (raw.file)
1072
+ event.file = raw.file;
1073
+ if (raw.status === "progress") {
1074
+ if (raw.loaded != null)
1075
+ event.loadedBytes = raw.loaded;
1076
+ if (raw.total != null)
1077
+ event.totalBytes = raw.total;
1078
+ if (raw.progress != null) {
1079
+ const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
1080
+ event.fraction = Math.max(0, Math.min(1, norm));
1081
+ }
1082
+ }
1083
+ this._onProgress(event);
1084
+ }
1085
+ }
1086
+ async function loadTransformersJs2() {
1087
+ try {
1088
+ return await import("@huggingface/transformers");
1089
+ } catch {
1090
+ throw new Error("CalibratedDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
1091
+ }
1092
+ }
1093
+ function readInputIds(tensor) {
1094
+ if (!tensor)
1095
+ return [];
1096
+ if (typeof tensor.tolist === "function") {
1097
+ const list = tensor.tolist();
1098
+ if (Array.isArray(list) && Array.isArray(list[0])) {
1099
+ return list[0].map((x) => Number(x));
1100
+ }
1101
+ if (Array.isArray(list))
1102
+ return list.map((x) => Number(x));
1103
+ }
1104
+ const dims = tensor.dims;
1105
+ const seq = dims[dims.length - 1] ?? 0;
1106
+ const data = tensor.data;
1107
+ const out = [];
1108
+ for (let i = 0;i < seq; i++)
1109
+ out.push(Number(data[i]));
1110
+ return out;
1111
+ }
1112
+ function normalizeDecoded(s) {
1113
+ return s.replace(/##/g, "").trim();
1114
+ }
1115
+ function readLogits(tensor) {
1116
+ if (typeof tensor.tolist === "function") {
1117
+ const list = tensor.tolist();
1118
+ if (Array.isArray(list) && Array.isArray(list[0]) && Array.isArray(list[0][0])) {
1119
+ return list[0];
1120
+ }
1121
+ if (Array.isArray(list) && Array.isArray(list[0])) {
1122
+ return list;
1123
+ }
1124
+ }
1125
+ const dims = tensor.dims;
1126
+ const seq = dims[dims.length - 2] ?? 0;
1127
+ const cls = dims[dims.length - 1] ?? 0;
1128
+ const data = tensor.data;
1129
+ const out = [];
1130
+ for (let t = 0;t < seq; t++) {
1131
+ const row = [];
1132
+ for (let c = 0;c < cls; c++) {
1133
+ row.push(Number(data[t * cls + c]));
1134
+ }
1135
+ out.push(row);
1136
+ }
1137
+ return out;
1138
+ }
845
1139
  // src/detectors/index.ts
846
1140
  function defaultDetector() {
847
1141
  const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
848
- return hasKey ? new RemoteDetector : new LocalDetector;
1142
+ return hasKey ? new RemoteDetector : new CalibratedDetector;
849
1143
  }
850
1144
  // src/plain.ts
851
1145
  function resolve(session, detector) {
@@ -896,4 +1190,4 @@ async function* deanonymizeStream(chunks, session) {
896
1190
  }
897
1191
 
898
1192
  // src/index.ts
899
- var VERSION = "0.1.4";
1193
+ var VERSION = "0.1.6";
@@ -751,10 +751,281 @@ class CompositeDetector {
751
751
  return out;
752
752
  }
753
753
  }
754
+ // src/detectors/calibrated.ts
755
+ var DEFAULT_MODEL3 = "joelbarmettler/gheim-ch-560m";
756
+ var DEFAULT_CHUNK_TOKENS2 = 510;
757
+ var DEFAULT_OVERLAP_TOKENS2 = 32;
758
+ function trimSpan2(text, start, end) {
759
+ while (start < end && /\s/.test(text[start] ?? ""))
760
+ start++;
761
+ while (end > start && /\s/.test(text[end - 1] ?? ""))
762
+ end--;
763
+ return [start, end];
764
+ }
765
+
766
+ class CalibratedDetector {
767
+ model;
768
+ oBias;
769
+ device;
770
+ dtype;
771
+ chunkTokens;
772
+ chunkOverlapTokens;
773
+ trimWhitespace;
774
+ actualDevice = null;
775
+ _model = null;
776
+ _tokenizer = null;
777
+ _id2label = {};
778
+ _oId = 0;
779
+ _loadPromise = null;
780
+ _onProgress;
781
+ constructor(opts = {}) {
782
+ this.model = opts.model ?? DEFAULT_MODEL3;
783
+ this.oBias = opts.oBias ?? 0.5;
784
+ this.device = opts.device ?? "auto";
785
+ this.dtype = opts.dtype ?? "fp32";
786
+ this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS2;
787
+ this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS2;
788
+ this.trimWhitespace = opts.trimWhitespace ?? true;
789
+ this._onProgress = opts.onProgress;
790
+ if (this.chunkOverlapTokens >= this.chunkTokens) {
791
+ throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than ` + `chunkTokens (${this.chunkTokens})`);
792
+ }
793
+ }
794
+ async load() {
795
+ if (this._model)
796
+ return;
797
+ if (this._loadPromise)
798
+ return this._loadPromise;
799
+ this._loadPromise = (async () => {
800
+ const mod = await loadTransformersJs2();
801
+ let preferredDevice = this.device;
802
+ if (this.device === "auto") {
803
+ const nav = typeof navigator !== "undefined" ? navigator : null;
804
+ preferredDevice = "wasm";
805
+ if (nav?.gpu?.requestAdapter) {
806
+ try {
807
+ const adapter = await nav.gpu.requestAdapter();
808
+ if (adapter)
809
+ preferredDevice = "webgpu";
810
+ } catch {}
811
+ }
812
+ }
813
+ const tokenizer = await mod.AutoTokenizer.from_pretrained(this.model);
814
+ const model = await mod.AutoModelForTokenClassification.from_pretrained(this.model, {
815
+ device: preferredDevice,
816
+ dtype: this.dtype,
817
+ progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, preferredDevice) : undefined
818
+ });
819
+ this._tokenizer = tokenizer;
820
+ this._model = model;
821
+ this._id2label = model.config.id2label;
822
+ const label2id = model.config.label2id;
823
+ this._oId = label2id?.["O"] ?? 0;
824
+ this.actualDevice = preferredDevice;
825
+ })();
826
+ return this._loadPromise;
827
+ }
828
+ async detect(text, opts) {
829
+ if (opts?.model && opts.model !== this.model) {
830
+ throw new Error(`CalibratedDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
831
+ }
832
+ if (!text)
833
+ return [];
834
+ await this.load();
835
+ return this._detectChunked(text);
836
+ }
837
+ async _detectChunked(text) {
838
+ if (text.length <= 1500) {
839
+ return await this._detectWindow(text, 0);
840
+ }
841
+ const stride = 1500 - 200;
842
+ const seen = new Set;
843
+ const out = [];
844
+ for (let s = 0;s < text.length; s += stride) {
845
+ const e = Math.min(s + 1500, text.length);
846
+ const sub = text.slice(s, e);
847
+ const subSpans = await this._detectWindow(sub, s);
848
+ for (const sp of subSpans) {
849
+ const key = `${sp.start}|${sp.end}|${sp.label}`;
850
+ if (seen.has(key))
851
+ continue;
852
+ seen.add(key);
853
+ out.push(sp);
854
+ }
855
+ if (e >= text.length)
856
+ break;
857
+ }
858
+ out.sort((a, b) => a.start - b.start || a.end - b.end);
859
+ return out;
860
+ }
861
+ async _detectWindow(text, charOffset) {
862
+ const tokenizer = this._tokenizer;
863
+ const model = this._model;
864
+ const enc = await tokenizer([text], {
865
+ padding: true,
866
+ truncation: true,
867
+ max_length: this.chunkTokens + 2
868
+ });
869
+ const inputs = {};
870
+ if (enc.input_ids)
871
+ inputs["input_ids"] = enc.input_ids;
872
+ if (enc.attention_mask)
873
+ inputs["attention_mask"] = enc.attention_mask;
874
+ const out = await model(inputs);
875
+ const logits = readLogits(out.logits);
876
+ if (this.oBias !== 0) {
877
+ for (let t = 0;t < logits.length; t++) {
878
+ const row = logits[t];
879
+ if (row === undefined)
880
+ continue;
881
+ const cur = row[this._oId];
882
+ if (cur !== undefined) {
883
+ row[this._oId] = cur - this.oBias;
884
+ }
885
+ }
886
+ }
887
+ const inputIds = readInputIds(enc.input_ids);
888
+ const tokenLabels = [];
889
+ for (let t = 0;t < logits.length; t++) {
890
+ const row = logits[t];
891
+ const id = inputIds[t];
892
+ if (!row || id === undefined)
893
+ continue;
894
+ let maxIdx = 0;
895
+ let maxVal = row[0];
896
+ for (let i = 1;i < row.length; i++) {
897
+ if (row[i] > maxVal) {
898
+ maxVal = row[i];
899
+ maxIdx = i;
900
+ }
901
+ }
902
+ tokenLabels.push({ id, label: this._id2label[maxIdx] ?? "O" });
903
+ }
904
+ const spans = [];
905
+ let cursor = 0;
906
+ let groupIds = [];
907
+ let groupCat = null;
908
+ const flush = () => {
909
+ if (groupCat === null || groupIds.length === 0)
910
+ return;
911
+ const surface = normalizeDecoded(tokenizer.decode(groupIds, { skip_special_tokens: true }));
912
+ groupIds = [];
913
+ const cat = groupCat;
914
+ groupCat = null;
915
+ if (!surface)
916
+ return;
917
+ const idx = text.indexOf(surface, cursor);
918
+ if (idx === -1)
919
+ return;
920
+ let s = idx;
921
+ let e = idx + surface.length;
922
+ cursor = e;
923
+ if (this.trimWhitespace)
924
+ [s, e] = trimSpan2(text, s, e);
925
+ if (s >= e)
926
+ return;
927
+ spans.push({
928
+ start: s + charOffset,
929
+ end: e + charOffset,
930
+ label: cat,
931
+ text: text.slice(s, e),
932
+ score: 1
933
+ });
934
+ };
935
+ for (const { id, label } of tokenLabels) {
936
+ const cat = label === "O" ? null : label.split("-", 2)[1] ?? null;
937
+ if (cat === groupCat && cat !== null) {
938
+ groupIds.push(id);
939
+ } else {
940
+ flush();
941
+ if (cat !== null) {
942
+ groupIds.push(id);
943
+ groupCat = cat;
944
+ }
945
+ }
946
+ }
947
+ flush();
948
+ return spans;
949
+ }
950
+ _emitProgress(raw, device) {
951
+ if (!this._onProgress)
952
+ return;
953
+ const event = {
954
+ phase: raw.status ?? "progress",
955
+ device
956
+ };
957
+ if (raw.file)
958
+ event.file = raw.file;
959
+ if (raw.status === "progress") {
960
+ if (raw.loaded != null)
961
+ event.loadedBytes = raw.loaded;
962
+ if (raw.total != null)
963
+ event.totalBytes = raw.total;
964
+ if (raw.progress != null) {
965
+ const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
966
+ event.fraction = Math.max(0, Math.min(1, norm));
967
+ }
968
+ }
969
+ this._onProgress(event);
970
+ }
971
+ }
972
+ async function loadTransformersJs2() {
973
+ try {
974
+ return await import("@huggingface/transformers");
975
+ } catch {
976
+ throw new Error("CalibratedDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
977
+ }
978
+ }
979
+ function readInputIds(tensor) {
980
+ if (!tensor)
981
+ return [];
982
+ if (typeof tensor.tolist === "function") {
983
+ const list = tensor.tolist();
984
+ if (Array.isArray(list) && Array.isArray(list[0])) {
985
+ return list[0].map((x) => Number(x));
986
+ }
987
+ if (Array.isArray(list))
988
+ return list.map((x) => Number(x));
989
+ }
990
+ const dims = tensor.dims;
991
+ const seq = dims[dims.length - 1] ?? 0;
992
+ const data = tensor.data;
993
+ const out = [];
994
+ for (let i = 0;i < seq; i++)
995
+ out.push(Number(data[i]));
996
+ return out;
997
+ }
998
+ function normalizeDecoded(s) {
999
+ return s.replace(/##/g, "").trim();
1000
+ }
1001
+ function readLogits(tensor) {
1002
+ if (typeof tensor.tolist === "function") {
1003
+ const list = tensor.tolist();
1004
+ if (Array.isArray(list) && Array.isArray(list[0]) && Array.isArray(list[0][0])) {
1005
+ return list[0];
1006
+ }
1007
+ if (Array.isArray(list) && Array.isArray(list[0])) {
1008
+ return list;
1009
+ }
1010
+ }
1011
+ const dims = tensor.dims;
1012
+ const seq = dims[dims.length - 2] ?? 0;
1013
+ const cls = dims[dims.length - 1] ?? 0;
1014
+ const data = tensor.data;
1015
+ const out = [];
1016
+ for (let t = 0;t < seq; t++) {
1017
+ const row = [];
1018
+ for (let c = 0;c < cls; c++) {
1019
+ row.push(Number(data[t * cls + c]));
1020
+ }
1021
+ out.push(row);
1022
+ }
1023
+ return out;
1024
+ }
754
1025
  // src/detectors/index.ts
755
1026
  function defaultDetector() {
756
1027
  const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
757
- return hasKey ? new RemoteDetector : new LocalDetector;
1028
+ return hasKey ? new RemoteDetector : new CalibratedDetector;
758
1029
  }
759
1030
 
760
1031
  // src/core/session.ts
@@ -815,8 +1086,8 @@ class Session {
815
1086
  }
816
1087
  out.push(text.slice(cursor));
817
1088
  let redacted = out.join("");
818
- redacted = recoverDuplicateOccurrences(redacted, this.mapping);
819
- return { redacted, merged };
1089
+ const recovered = recoverDuplicateOccurrences(text, redacted, merged, this.mapping);
1090
+ return { redacted: recovered.redacted, merged: recovered.merged };
820
1091
  }
821
1092
  restore(text) {
822
1093
  if (Object.keys(this.mapping).length === 0)
@@ -875,14 +1146,36 @@ function mergeAdjacent(text, spans) {
875
1146
  function escapeRegex(s) {
876
1147
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
877
1148
  }
878
- function recoverDuplicateOccurrences(redacted, mapping) {
1149
+ function recoverDuplicateOccurrences(text, redacted, merged, mapping) {
1150
+ const surfaceToLabel = new Map;
1151
+ for (const m of merged)
1152
+ surfaceToLabel.set(text.slice(m.start, m.end), m.label);
1153
+ const covered = merged.map((m) => [m.start, m.end]);
1154
+ const overlaps = (s, e) => covered.some(([cs, ce]) => s < ce && e > cs);
879
1155
  const entries = Object.entries(mapping).filter(([, surface]) => surface && surface.length > 0).sort((a, b) => b[1].length - a[1].length);
880
1156
  let result = redacted;
1157
+ const additional = [];
881
1158
  for (const [sentinel, surface] of entries) {
882
- const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`, "gu");
883
- result = result.replace(pattern, sentinel);
1159
+ const boundaryPattern = `(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`;
1160
+ result = result.replace(new RegExp(boundaryPattern, "gu"), sentinel);
1161
+ const label = surfaceToLabel.get(surface);
1162
+ if (!label)
1163
+ continue;
1164
+ const origRe = new RegExp(boundaryPattern, "gu");
1165
+ let match;
1166
+ while ((match = origRe.exec(text)) !== null) {
1167
+ const start = match.index;
1168
+ const end = start + surface.length;
1169
+ if (overlaps(start, end))
1170
+ continue;
1171
+ additional.push({ start, end, label });
1172
+ covered.push([start, end]);
1173
+ }
884
1174
  }
885
- return result;
1175
+ if (additional.length === 0)
1176
+ return { redacted: result, merged };
1177
+ const augmented = [...merged, ...additional].sort((a, b) => a.start - b.start);
1178
+ return { redacted: result, merged: augmented };
886
1179
  }
887
1180
 
888
1181
  // src/openai/_base.ts