gheim 0.1.3 → 0.1.5
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 +290 -4
- package/dist/cjs/openai/index.cjs +287 -2
- package/dist/esm/index.js +290 -4
- package/dist/esm/openai/index.js +287 -2
- 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
|
|
|
@@ -155,7 +156,9 @@ class Session {
|
|
|
155
156
|
cursor = end;
|
|
156
157
|
}
|
|
157
158
|
out.push(text.slice(cursor));
|
|
158
|
-
|
|
159
|
+
let redacted = out.join("");
|
|
160
|
+
redacted = recoverDuplicateOccurrences(redacted, this.mapping);
|
|
161
|
+
return { redacted, merged };
|
|
159
162
|
}
|
|
160
163
|
restore(text) {
|
|
161
164
|
if (Object.keys(this.mapping).length === 0)
|
|
@@ -211,6 +214,18 @@ function mergeAdjacent(text, spans) {
|
|
|
211
214
|
}
|
|
212
215
|
return out;
|
|
213
216
|
}
|
|
217
|
+
function escapeRegex(s) {
|
|
218
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
219
|
+
}
|
|
220
|
+
function recoverDuplicateOccurrences(redacted, mapping) {
|
|
221
|
+
const entries = Object.entries(mapping).filter(([, surface]) => surface && surface.length > 0).sort((a, b) => b[1].length - a[1].length);
|
|
222
|
+
let result = redacted;
|
|
223
|
+
for (const [sentinel, surface] of entries) {
|
|
224
|
+
const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`, "gu");
|
|
225
|
+
result = result.replace(pattern, sentinel);
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
214
229
|
// src/core/stream.ts
|
|
215
230
|
var SENTINEL_AT = /<([A-Z][A-Z0-9_]*)_(\d+)>/y;
|
|
216
231
|
var MAX_SENTINEL_LEN = 64;
|
|
@@ -828,10 +843,281 @@ class CompositeDetector {
|
|
|
828
843
|
return out;
|
|
829
844
|
}
|
|
830
845
|
}
|
|
846
|
+
// src/detectors/calibrated.ts
|
|
847
|
+
var DEFAULT_MODEL3 = "joelbarmettler/gheim-ch-560m";
|
|
848
|
+
var DEFAULT_CHUNK_TOKENS2 = 510;
|
|
849
|
+
var DEFAULT_OVERLAP_TOKENS2 = 32;
|
|
850
|
+
function trimSpan2(text, start, end) {
|
|
851
|
+
while (start < end && /\s/.test(text[start] ?? ""))
|
|
852
|
+
start++;
|
|
853
|
+
while (end > start && /\s/.test(text[end - 1] ?? ""))
|
|
854
|
+
end--;
|
|
855
|
+
return [start, end];
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
class CalibratedDetector {
|
|
859
|
+
model;
|
|
860
|
+
oBias;
|
|
861
|
+
device;
|
|
862
|
+
dtype;
|
|
863
|
+
chunkTokens;
|
|
864
|
+
chunkOverlapTokens;
|
|
865
|
+
trimWhitespace;
|
|
866
|
+
actualDevice = null;
|
|
867
|
+
_model = null;
|
|
868
|
+
_tokenizer = null;
|
|
869
|
+
_id2label = {};
|
|
870
|
+
_oId = 0;
|
|
871
|
+
_loadPromise = null;
|
|
872
|
+
_onProgress;
|
|
873
|
+
constructor(opts = {}) {
|
|
874
|
+
this.model = opts.model ?? DEFAULT_MODEL3;
|
|
875
|
+
this.oBias = opts.oBias ?? 0.5;
|
|
876
|
+
this.device = opts.device ?? "auto";
|
|
877
|
+
this.dtype = opts.dtype ?? "fp32";
|
|
878
|
+
this.chunkTokens = opts.chunkTokens ?? DEFAULT_CHUNK_TOKENS2;
|
|
879
|
+
this.chunkOverlapTokens = opts.chunkOverlapTokens ?? DEFAULT_OVERLAP_TOKENS2;
|
|
880
|
+
this.trimWhitespace = opts.trimWhitespace ?? true;
|
|
881
|
+
this._onProgress = opts.onProgress;
|
|
882
|
+
if (this.chunkOverlapTokens >= this.chunkTokens) {
|
|
883
|
+
throw new Error(`chunkOverlapTokens (${this.chunkOverlapTokens}) must be smaller than ` + `chunkTokens (${this.chunkTokens})`);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
async load() {
|
|
887
|
+
if (this._model)
|
|
888
|
+
return;
|
|
889
|
+
if (this._loadPromise)
|
|
890
|
+
return this._loadPromise;
|
|
891
|
+
this._loadPromise = (async () => {
|
|
892
|
+
const mod = await loadTransformersJs2();
|
|
893
|
+
let preferredDevice = this.device;
|
|
894
|
+
if (this.device === "auto") {
|
|
895
|
+
const nav = typeof navigator !== "undefined" ? navigator : null;
|
|
896
|
+
preferredDevice = "wasm";
|
|
897
|
+
if (nav?.gpu?.requestAdapter) {
|
|
898
|
+
try {
|
|
899
|
+
const adapter = await nav.gpu.requestAdapter();
|
|
900
|
+
if (adapter)
|
|
901
|
+
preferredDevice = "webgpu";
|
|
902
|
+
} catch {}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const tokenizer = await mod.AutoTokenizer.from_pretrained(this.model);
|
|
906
|
+
const model = await mod.AutoModelForTokenClassification.from_pretrained(this.model, {
|
|
907
|
+
device: preferredDevice,
|
|
908
|
+
dtype: this.dtype,
|
|
909
|
+
progress_callback: this._onProgress ? (raw) => this._emitProgress(raw, preferredDevice) : undefined
|
|
910
|
+
});
|
|
911
|
+
this._tokenizer = tokenizer;
|
|
912
|
+
this._model = model;
|
|
913
|
+
this._id2label = model.config.id2label;
|
|
914
|
+
const label2id = model.config.label2id;
|
|
915
|
+
this._oId = label2id?.["O"] ?? 0;
|
|
916
|
+
this.actualDevice = preferredDevice;
|
|
917
|
+
})();
|
|
918
|
+
return this._loadPromise;
|
|
919
|
+
}
|
|
920
|
+
async detect(text, opts) {
|
|
921
|
+
if (opts?.model && opts.model !== this.model) {
|
|
922
|
+
throw new Error(`CalibratedDetector was constructed with model='${this.model}'; ` + `per-call model override ('${opts.model}') not supported.`);
|
|
923
|
+
}
|
|
924
|
+
if (!text)
|
|
925
|
+
return [];
|
|
926
|
+
await this.load();
|
|
927
|
+
return this._detectChunked(text);
|
|
928
|
+
}
|
|
929
|
+
async _detectChunked(text) {
|
|
930
|
+
if (text.length <= 1500) {
|
|
931
|
+
return await this._detectWindow(text, 0);
|
|
932
|
+
}
|
|
933
|
+
const stride = 1500 - 200;
|
|
934
|
+
const seen = new Set;
|
|
935
|
+
const out = [];
|
|
936
|
+
for (let s = 0;s < text.length; s += stride) {
|
|
937
|
+
const e = Math.min(s + 1500, text.length);
|
|
938
|
+
const sub = text.slice(s, e);
|
|
939
|
+
const subSpans = await this._detectWindow(sub, s);
|
|
940
|
+
for (const sp of subSpans) {
|
|
941
|
+
const key = `${sp.start}|${sp.end}|${sp.label}`;
|
|
942
|
+
if (seen.has(key))
|
|
943
|
+
continue;
|
|
944
|
+
seen.add(key);
|
|
945
|
+
out.push(sp);
|
|
946
|
+
}
|
|
947
|
+
if (e >= text.length)
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
out.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
951
|
+
return out;
|
|
952
|
+
}
|
|
953
|
+
async _detectWindow(text, charOffset) {
|
|
954
|
+
const tokenizer = this._tokenizer;
|
|
955
|
+
const model = this._model;
|
|
956
|
+
const enc = await tokenizer([text], {
|
|
957
|
+
padding: true,
|
|
958
|
+
truncation: true,
|
|
959
|
+
max_length: this.chunkTokens + 2
|
|
960
|
+
});
|
|
961
|
+
const inputs = {};
|
|
962
|
+
if (enc.input_ids)
|
|
963
|
+
inputs["input_ids"] = enc.input_ids;
|
|
964
|
+
if (enc.attention_mask)
|
|
965
|
+
inputs["attention_mask"] = enc.attention_mask;
|
|
966
|
+
const out = await model(inputs);
|
|
967
|
+
const logits = readLogits(out.logits);
|
|
968
|
+
if (this.oBias !== 0) {
|
|
969
|
+
for (let t = 0;t < logits.length; t++) {
|
|
970
|
+
const row = logits[t];
|
|
971
|
+
if (row === undefined)
|
|
972
|
+
continue;
|
|
973
|
+
const cur = row[this._oId];
|
|
974
|
+
if (cur !== undefined) {
|
|
975
|
+
row[this._oId] = cur - this.oBias;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const inputIds = readInputIds(enc.input_ids);
|
|
980
|
+
const tokenLabels = [];
|
|
981
|
+
for (let t = 0;t < logits.length; t++) {
|
|
982
|
+
const row = logits[t];
|
|
983
|
+
const id = inputIds[t];
|
|
984
|
+
if (!row || id === undefined)
|
|
985
|
+
continue;
|
|
986
|
+
let maxIdx = 0;
|
|
987
|
+
let maxVal = row[0];
|
|
988
|
+
for (let i = 1;i < row.length; i++) {
|
|
989
|
+
if (row[i] > maxVal) {
|
|
990
|
+
maxVal = row[i];
|
|
991
|
+
maxIdx = i;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
tokenLabels.push({ id, label: this._id2label[maxIdx] ?? "O" });
|
|
995
|
+
}
|
|
996
|
+
const spans = [];
|
|
997
|
+
let cursor = 0;
|
|
998
|
+
let groupIds = [];
|
|
999
|
+
let groupCat = null;
|
|
1000
|
+
const flush = () => {
|
|
1001
|
+
if (groupCat === null || groupIds.length === 0)
|
|
1002
|
+
return;
|
|
1003
|
+
const surface = normalizeDecoded(tokenizer.decode(groupIds, { skip_special_tokens: true }));
|
|
1004
|
+
groupIds = [];
|
|
1005
|
+
const cat = groupCat;
|
|
1006
|
+
groupCat = null;
|
|
1007
|
+
if (!surface)
|
|
1008
|
+
return;
|
|
1009
|
+
const idx = text.indexOf(surface, cursor);
|
|
1010
|
+
if (idx === -1)
|
|
1011
|
+
return;
|
|
1012
|
+
let s = idx;
|
|
1013
|
+
let e = idx + surface.length;
|
|
1014
|
+
cursor = e;
|
|
1015
|
+
if (this.trimWhitespace)
|
|
1016
|
+
[s, e] = trimSpan2(text, s, e);
|
|
1017
|
+
if (s >= e)
|
|
1018
|
+
return;
|
|
1019
|
+
spans.push({
|
|
1020
|
+
start: s + charOffset,
|
|
1021
|
+
end: e + charOffset,
|
|
1022
|
+
label: cat,
|
|
1023
|
+
text: text.slice(s, e),
|
|
1024
|
+
score: 1
|
|
1025
|
+
});
|
|
1026
|
+
};
|
|
1027
|
+
for (const { id, label } of tokenLabels) {
|
|
1028
|
+
const cat = label === "O" ? null : label.split("-", 2)[1] ?? null;
|
|
1029
|
+
if (cat === groupCat && cat !== null) {
|
|
1030
|
+
groupIds.push(id);
|
|
1031
|
+
} else {
|
|
1032
|
+
flush();
|
|
1033
|
+
if (cat !== null) {
|
|
1034
|
+
groupIds.push(id);
|
|
1035
|
+
groupCat = cat;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
flush();
|
|
1040
|
+
return spans;
|
|
1041
|
+
}
|
|
1042
|
+
_emitProgress(raw, device) {
|
|
1043
|
+
if (!this._onProgress)
|
|
1044
|
+
return;
|
|
1045
|
+
const event = {
|
|
1046
|
+
phase: raw.status ?? "progress",
|
|
1047
|
+
device
|
|
1048
|
+
};
|
|
1049
|
+
if (raw.file)
|
|
1050
|
+
event.file = raw.file;
|
|
1051
|
+
if (raw.status === "progress") {
|
|
1052
|
+
if (raw.loaded != null)
|
|
1053
|
+
event.loadedBytes = raw.loaded;
|
|
1054
|
+
if (raw.total != null)
|
|
1055
|
+
event.totalBytes = raw.total;
|
|
1056
|
+
if (raw.progress != null) {
|
|
1057
|
+
const norm = raw.progress > 1 ? raw.progress / 100 : raw.progress;
|
|
1058
|
+
event.fraction = Math.max(0, Math.min(1, norm));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
this._onProgress(event);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async function loadTransformersJs2() {
|
|
1065
|
+
try {
|
|
1066
|
+
return await import("@huggingface/transformers");
|
|
1067
|
+
} catch {
|
|
1068
|
+
throw new Error("CalibratedDetector requires `@huggingface/transformers`. Install with: bun add @huggingface/transformers");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function readInputIds(tensor) {
|
|
1072
|
+
if (!tensor)
|
|
1073
|
+
return [];
|
|
1074
|
+
if (typeof tensor.tolist === "function") {
|
|
1075
|
+
const list = tensor.tolist();
|
|
1076
|
+
if (Array.isArray(list) && Array.isArray(list[0])) {
|
|
1077
|
+
return list[0].map((x) => Number(x));
|
|
1078
|
+
}
|
|
1079
|
+
if (Array.isArray(list))
|
|
1080
|
+
return list.map((x) => Number(x));
|
|
1081
|
+
}
|
|
1082
|
+
const dims = tensor.dims;
|
|
1083
|
+
const seq = dims[dims.length - 1] ?? 0;
|
|
1084
|
+
const data = tensor.data;
|
|
1085
|
+
const out = [];
|
|
1086
|
+
for (let i = 0;i < seq; i++)
|
|
1087
|
+
out.push(Number(data[i]));
|
|
1088
|
+
return out;
|
|
1089
|
+
}
|
|
1090
|
+
function normalizeDecoded(s) {
|
|
1091
|
+
return s.replace(/##/g, "").trim();
|
|
1092
|
+
}
|
|
1093
|
+
function readLogits(tensor) {
|
|
1094
|
+
if (typeof tensor.tolist === "function") {
|
|
1095
|
+
const list = tensor.tolist();
|
|
1096
|
+
if (Array.isArray(list) && Array.isArray(list[0]) && Array.isArray(list[0][0])) {
|
|
1097
|
+
return list[0];
|
|
1098
|
+
}
|
|
1099
|
+
if (Array.isArray(list) && Array.isArray(list[0])) {
|
|
1100
|
+
return list;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const dims = tensor.dims;
|
|
1104
|
+
const seq = dims[dims.length - 2] ?? 0;
|
|
1105
|
+
const cls = dims[dims.length - 1] ?? 0;
|
|
1106
|
+
const data = tensor.data;
|
|
1107
|
+
const out = [];
|
|
1108
|
+
for (let t = 0;t < seq; t++) {
|
|
1109
|
+
const row = [];
|
|
1110
|
+
for (let c = 0;c < cls; c++) {
|
|
1111
|
+
row.push(Number(data[t * cls + c]));
|
|
1112
|
+
}
|
|
1113
|
+
out.push(row);
|
|
1114
|
+
}
|
|
1115
|
+
return out;
|
|
1116
|
+
}
|
|
831
1117
|
// src/detectors/index.ts
|
|
832
1118
|
function defaultDetector() {
|
|
833
1119
|
const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
|
|
834
|
-
return hasKey ? new RemoteDetector : new
|
|
1120
|
+
return hasKey ? new RemoteDetector : new CalibratedDetector;
|
|
835
1121
|
}
|
|
836
1122
|
// src/plain.ts
|
|
837
1123
|
function resolve(session, detector) {
|
|
@@ -882,4 +1168,4 @@ async function* deanonymizeStream(chunks, session) {
|
|
|
882
1168
|
}
|
|
883
1169
|
|
|
884
1170
|
// src/index.ts
|
|
885
|
-
var VERSION = "0.1.
|
|
1171
|
+
var VERSION = "0.1.5";
|
|
@@ -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
|
|
@@ -814,7 +1085,9 @@ class Session {
|
|
|
814
1085
|
cursor = end;
|
|
815
1086
|
}
|
|
816
1087
|
out.push(text.slice(cursor));
|
|
817
|
-
|
|
1088
|
+
let redacted = out.join("");
|
|
1089
|
+
redacted = recoverDuplicateOccurrences(redacted, this.mapping);
|
|
1090
|
+
return { redacted, merged };
|
|
818
1091
|
}
|
|
819
1092
|
restore(text) {
|
|
820
1093
|
if (Object.keys(this.mapping).length === 0)
|
|
@@ -870,6 +1143,18 @@ function mergeAdjacent(text, spans) {
|
|
|
870
1143
|
}
|
|
871
1144
|
return out;
|
|
872
1145
|
}
|
|
1146
|
+
function escapeRegex(s) {
|
|
1147
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1148
|
+
}
|
|
1149
|
+
function recoverDuplicateOccurrences(redacted, mapping) {
|
|
1150
|
+
const entries = Object.entries(mapping).filter(([, surface]) => surface && surface.length > 0).sort((a, b) => b[1].length - a[1].length);
|
|
1151
|
+
let result = redacted;
|
|
1152
|
+
for (const [sentinel, surface] of entries) {
|
|
1153
|
+
const pattern = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegex(surface)}(?![\\p{L}\\p{N}_])`, "gu");
|
|
1154
|
+
result = result.replace(pattern, sentinel);
|
|
1155
|
+
}
|
|
1156
|
+
return result;
|
|
1157
|
+
}
|
|
873
1158
|
|
|
874
1159
|
// src/openai/_base.ts
|
|
875
1160
|
function resolveCallSession(body, defaultDet) {
|