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.
- package/dist/cjs/index.cjs +303 -9
- package/dist/cjs/openai/index.cjs +300 -7
- package/dist/esm/index.js +303 -9
- package/dist/esm/openai/index.js +300 -7
- package/dist/types/core/session.d.ts.map +1 -1
- package/dist/types/detectors/calibrated.d.ts +78 -0
- package/dist/types/detectors/calibrated.d.ts.map +1 -0
- package/dist/types/detectors/index.d.ts +8 -1
- package/dist/types/detectors/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
224
|
-
result = result.replace(
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
883
|
-
result = result.replace(
|
|
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
|
-
|
|
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
|