gheim 0.1.8 → 0.1.9
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/README.md +96 -6
- package/dist/cjs/index.cjs +183 -30
- package/dist/cjs/openai/index.cjs +168 -16
- package/dist/esm/index.js +183 -38
- package/dist/esm/openai/index.js +168 -24
- package/dist/types/detectors/base.d.ts +13 -0
- package/dist/types/detectors/base.d.ts.map +1 -1
- package/dist/types/detectors/calibrated.d.ts +0 -28
- package/dist/types/detectors/calibrated.d.ts.map +1 -1
- package/dist/types/detectors/composite.d.ts +50 -0
- package/dist/types/detectors/composite.d.ts.map +1 -1
- package/dist/types/detectors/index.d.ts +10 -6
- package/dist/types/detectors/index.d.ts.map +1 -1
- package/dist/types/detectors/local.d.ts +0 -14
- package/dist/types/detectors/local.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined")
|
|
5
|
-
return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
1
|
// src/core/sentinels.ts
|
|
10
2
|
var LABEL_TO_TAG = {
|
|
11
3
|
private_person: "PERSON",
|
|
@@ -264,6 +256,36 @@ function trimSpan(text, start, end) {
|
|
|
264
256
|
return null;
|
|
265
257
|
return { start, end };
|
|
266
258
|
}
|
|
259
|
+
var WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
260
|
+
var isWordChar = (ch) => ch !== undefined && WORD_CHAR.test(ch);
|
|
261
|
+
function snapToWords(text, spans) {
|
|
262
|
+
const snapped = spans.map((sp) => {
|
|
263
|
+
let { start, end } = sp;
|
|
264
|
+
while (start > 0 && isWordChar(text[start - 1]) && isWordChar(text[start]))
|
|
265
|
+
start -= 1;
|
|
266
|
+
while (end < text.length && isWordChar(text[end - 1]) && isWordChar(text[end]))
|
|
267
|
+
end += 1;
|
|
268
|
+
return start === sp.start && end === sp.end ? sp : { ...sp, start, end, text: text.slice(start, end) };
|
|
269
|
+
});
|
|
270
|
+
snapped.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
271
|
+
const merged = [];
|
|
272
|
+
const lastByLabel = new Map;
|
|
273
|
+
for (const sp of snapped) {
|
|
274
|
+
const i = lastByLabel.get(sp.label);
|
|
275
|
+
const prev = i === undefined ? undefined : merged[i];
|
|
276
|
+
if (i !== undefined && prev !== undefined && sp.start <= prev.end) {
|
|
277
|
+
const end = Math.max(prev.end, sp.end);
|
|
278
|
+
const wPrev = prev.end - prev.start;
|
|
279
|
+
const wSp = sp.end - sp.start;
|
|
280
|
+
const score = ((prev.score ?? 1) * wPrev + (sp.score ?? 1) * wSp) / (wPrev + wSp);
|
|
281
|
+
merged[i] = { ...prev, end, text: text.slice(prev.start, end), score };
|
|
282
|
+
} else {
|
|
283
|
+
lastByLabel.set(sp.label, merged.length);
|
|
284
|
+
merged.push(sp);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return merged.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
288
|
+
}
|
|
267
289
|
|
|
268
290
|
// src/detectors/remote.ts
|
|
269
291
|
var DEFAULT_BASE_URL = "https://api.gheim.ch";
|
|
@@ -512,10 +534,7 @@ class LocalDetector {
|
|
|
512
534
|
});
|
|
513
535
|
}
|
|
514
536
|
}
|
|
515
|
-
|
|
516
|
-
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
517
|
-
}
|
|
518
|
-
return out;
|
|
537
|
+
return out.map((spans, i) => snapToWords(texts[i] ?? "", spans));
|
|
519
538
|
}
|
|
520
539
|
async chunkText(text, tokenizer) {
|
|
521
540
|
if (tokenizer) {
|
|
@@ -792,21 +811,136 @@ function maskSpans(text, spans) {
|
|
|
792
811
|
return chars.join("");
|
|
793
812
|
}
|
|
794
813
|
|
|
814
|
+
class CustomPattern {
|
|
815
|
+
label;
|
|
816
|
+
group;
|
|
817
|
+
name;
|
|
818
|
+
regex;
|
|
819
|
+
validator;
|
|
820
|
+
constructor(opts) {
|
|
821
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(opts.label)) {
|
|
822
|
+
throw new Error(`CustomPattern label ${JSON.stringify(opts.label)} must start with a letter and contain ` + "only letters, digits and underscores (it becomes a <TAG_1> placeholder).");
|
|
823
|
+
}
|
|
824
|
+
this.label = opts.label;
|
|
825
|
+
this.name = opts.name ?? opts.label;
|
|
826
|
+
this.group = opts.group ?? 0;
|
|
827
|
+
this.validator = opts.validator;
|
|
828
|
+
const source = typeof opts.regex === "string" ? opts.regex : opts.regex.source;
|
|
829
|
+
const base = typeof opts.regex === "string" ? opts.flags ?? "i" : opts.regex.flags;
|
|
830
|
+
const flags = [...new Set([...base, "g", "d"])].join("");
|
|
831
|
+
this.regex = new RegExp(source, flags);
|
|
832
|
+
const groups = (new RegExp(`${source}|`, flags.replace(/[gd]/g, "")).exec("") ?? []).length - 1;
|
|
833
|
+
if (!Number.isInteger(this.group) || this.group < 0 || this.group > groups) {
|
|
834
|
+
throw new Error(`CustomPattern ${JSON.stringify(this.name)}: group ${this.group} does not exist ` + `(the pattern has ${groups}).`);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
find(text) {
|
|
838
|
+
const out = [];
|
|
839
|
+
this.regex.lastIndex = 0;
|
|
840
|
+
let m;
|
|
841
|
+
while ((m = this.regex.exec(text)) !== null) {
|
|
842
|
+
if (m[0] === "") {
|
|
843
|
+
this.regex.lastIndex++;
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const range = m.indices[this.group];
|
|
847
|
+
if (!range || range[0] === range[1])
|
|
848
|
+
continue;
|
|
849
|
+
const [start, end] = range;
|
|
850
|
+
const value = text.slice(start, end);
|
|
851
|
+
if (this.validator && !this.validator(value))
|
|
852
|
+
continue;
|
|
853
|
+
out.push({ start, end, label: this.label, text: value, score: 1 });
|
|
854
|
+
}
|
|
855
|
+
return out;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
var NAME_PART = /[A-ZÀ-Ý][\p{L}\p{N}_'’-]{3,}/gu;
|
|
859
|
+
var HONORIFICS = new Set([
|
|
860
|
+
"frau",
|
|
861
|
+
"herr",
|
|
862
|
+
"herrn",
|
|
863
|
+
"madame",
|
|
864
|
+
"monsieur",
|
|
865
|
+
"signora",
|
|
866
|
+
"signor",
|
|
867
|
+
"signore",
|
|
868
|
+
"dott",
|
|
869
|
+
"dottor",
|
|
870
|
+
"dottoressa",
|
|
871
|
+
"doktor",
|
|
872
|
+
"docteur",
|
|
873
|
+
"prof",
|
|
874
|
+
"professor",
|
|
875
|
+
"mister",
|
|
876
|
+
"miss",
|
|
877
|
+
"mrs"
|
|
878
|
+
]);
|
|
879
|
+
function escapeRegExp(s) {
|
|
880
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
881
|
+
}
|
|
882
|
+
function propagateNames(text, spans, surnames) {
|
|
883
|
+
const names = new Set;
|
|
884
|
+
for (const sp of spans) {
|
|
885
|
+
if (sp.label !== "private_person")
|
|
886
|
+
continue;
|
|
887
|
+
const value = text.slice(sp.start, sp.end).trim();
|
|
888
|
+
if (value.length >= 4)
|
|
889
|
+
names.add(value);
|
|
890
|
+
if (surnames) {
|
|
891
|
+
for (const part of value.match(NAME_PART) ?? []) {
|
|
892
|
+
if (!HONORIFICS.has(part.toLowerCase()))
|
|
893
|
+
names.add(part);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
let result = [...spans];
|
|
898
|
+
for (const name of [...names].sort((a, b) => b.length - a.length)) {
|
|
899
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegExp(name)}(?![\\p{L}\\p{N}_])`, "gu");
|
|
900
|
+
let m;
|
|
901
|
+
while ((m = re.exec(text)) !== null) {
|
|
902
|
+
const start = m.index;
|
|
903
|
+
const end = start + m[0].length;
|
|
904
|
+
const hits = result.filter((sp) => start < sp.end && sp.start < end);
|
|
905
|
+
if (hits.some((sp) => sp.start <= start && end <= sp.end))
|
|
906
|
+
continue;
|
|
907
|
+
if (hits.some((sp) => sp.start < start || end < sp.end))
|
|
908
|
+
continue;
|
|
909
|
+
result = result.filter((sp) => !hits.includes(sp));
|
|
910
|
+
result.push({ start, end, label: "private_person", text: m[0], score: 1 });
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
915
|
+
|
|
795
916
|
class CompositeDetector {
|
|
796
917
|
model;
|
|
918
|
+
patterns;
|
|
919
|
+
propagateNames;
|
|
920
|
+
propagateSurnames;
|
|
797
921
|
constructor(opts) {
|
|
798
922
|
if (!opts || !opts.model) {
|
|
799
923
|
throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
|
|
800
924
|
}
|
|
801
925
|
this.model = opts.model;
|
|
926
|
+
this.patterns = opts.patterns ?? [];
|
|
927
|
+
this.propagateNames = opts.propagateNames ?? true;
|
|
928
|
+
this.propagateSurnames = opts.propagateSurnames ?? false;
|
|
802
929
|
}
|
|
803
930
|
async detect(text, opts) {
|
|
804
931
|
if (!text)
|
|
805
932
|
return [];
|
|
806
933
|
const regexSpans = findRegexSpans(text);
|
|
934
|
+
const extra = this.patterns.flatMap((p) => p.find(text));
|
|
935
|
+
extra.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
936
|
+
for (const sp of extra) {
|
|
937
|
+
if (!regexSpans.some((o) => sp.start < o.end && o.start < sp.end))
|
|
938
|
+
regexSpans.push(sp);
|
|
939
|
+
}
|
|
940
|
+
regexSpans.sort((a, b) => a.start - b.start);
|
|
807
941
|
const masked = maskSpans(text, regexSpans);
|
|
808
942
|
const modelSpans = await this.model.detect(masked, opts);
|
|
809
|
-
|
|
943
|
+
let out = [...regexSpans];
|
|
810
944
|
const regexRanges = regexSpans.map((s) => [s.start, s.end]);
|
|
811
945
|
for (const sp of modelSpans) {
|
|
812
946
|
const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
|
|
@@ -814,6 +948,8 @@ class CompositeDetector {
|
|
|
814
948
|
continue;
|
|
815
949
|
out.push(sp);
|
|
816
950
|
}
|
|
951
|
+
if (this.propagateNames)
|
|
952
|
+
out = propagateNames(text, out, this.propagateSurnames);
|
|
817
953
|
out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
818
954
|
return out;
|
|
819
955
|
}
|
|
@@ -903,7 +1039,7 @@ class CalibratedDetector {
|
|
|
903
1039
|
if (!text)
|
|
904
1040
|
return [];
|
|
905
1041
|
await this.load();
|
|
906
|
-
return this._detectChunked(text);
|
|
1042
|
+
return snapToWords(text, await this._detectChunked(text));
|
|
907
1043
|
}
|
|
908
1044
|
async _detectChunked(text) {
|
|
909
1045
|
if (text.length <= 1500) {
|
|
@@ -970,15 +1106,21 @@ class CalibratedDetector {
|
|
|
970
1106
|
maxIdx = i;
|
|
971
1107
|
}
|
|
972
1108
|
}
|
|
973
|
-
|
|
1109
|
+
let denom = 0;
|
|
1110
|
+
for (let i = 0;i < row.length; i++)
|
|
1111
|
+
denom += Math.exp(row[i] - maxVal);
|
|
1112
|
+
tokenLabels.push({ id, label: this._id2label[maxIdx] ?? "O", prob: 1 / denom });
|
|
974
1113
|
}
|
|
975
1114
|
const spans = [];
|
|
976
1115
|
let cursor = 0;
|
|
977
1116
|
let groupIds = [];
|
|
1117
|
+
let groupProbs = [];
|
|
978
1118
|
let groupCat = null;
|
|
979
1119
|
const flush = () => {
|
|
980
1120
|
if (groupCat === null || groupIds.length === 0)
|
|
981
1121
|
return;
|
|
1122
|
+
const score = groupProbs.reduce((a, b) => a + b, 0) / groupProbs.length;
|
|
1123
|
+
groupProbs = [];
|
|
982
1124
|
const surface = normalizeDecoded(tokenizer.decode(groupIds, { skip_special_tokens: true }));
|
|
983
1125
|
groupIds = [];
|
|
984
1126
|
const cat = groupCat;
|
|
@@ -1000,17 +1142,19 @@ class CalibratedDetector {
|
|
|
1000
1142
|
end: e + charOffset,
|
|
1001
1143
|
label: cat,
|
|
1002
1144
|
text: text.slice(s, e),
|
|
1003
|
-
score
|
|
1145
|
+
score
|
|
1004
1146
|
});
|
|
1005
1147
|
};
|
|
1006
|
-
for (const { id, label } of tokenLabels) {
|
|
1148
|
+
for (const { id, label, prob } of tokenLabels) {
|
|
1007
1149
|
const cat = label === "O" ? null : label.split("-", 2)[1] ?? null;
|
|
1008
1150
|
if (cat === groupCat && cat !== null) {
|
|
1009
1151
|
groupIds.push(id);
|
|
1152
|
+
groupProbs.push(prob);
|
|
1010
1153
|
} else {
|
|
1011
1154
|
flush();
|
|
1012
1155
|
if (cat !== null) {
|
|
1013
1156
|
groupIds.push(id);
|
|
1157
|
+
groupProbs.push(prob);
|
|
1014
1158
|
groupCat = cat;
|
|
1015
1159
|
}
|
|
1016
1160
|
}
|
|
@@ -1096,7 +1240,7 @@ function readLogits(tensor) {
|
|
|
1096
1240
|
// src/detectors/index.ts
|
|
1097
1241
|
function defaultDetector() {
|
|
1098
1242
|
const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
|
|
1099
|
-
return hasKey ? new RemoteDetector : new CalibratedDetector;
|
|
1243
|
+
return hasKey ? new RemoteDetector : new CompositeDetector({ model: new CalibratedDetector });
|
|
1100
1244
|
}
|
|
1101
1245
|
// src/plain.ts
|
|
1102
1246
|
function resolve(session, detector) {
|
|
@@ -1149,25 +1293,26 @@ async function* deanonymizeStream(chunks, session) {
|
|
|
1149
1293
|
// src/index.ts
|
|
1150
1294
|
var VERSION = "0.1.6";
|
|
1151
1295
|
export {
|
|
1152
|
-
|
|
1153
|
-
parseSentinel,
|
|
1154
|
-
mergeAdjacent,
|
|
1155
|
-
labelTag,
|
|
1156
|
-
isPossibleSentinelPrefix,
|
|
1157
|
-
defaultDetector,
|
|
1158
|
-
deanonymizeText,
|
|
1159
|
-
deanonymizeStream,
|
|
1160
|
-
anonymizeText,
|
|
1161
|
-
anonymizeMessages,
|
|
1162
|
-
VERSION,
|
|
1163
|
-
StreamDeanonymizer,
|
|
1164
|
-
Session,
|
|
1165
|
-
SENTINEL_RE,
|
|
1166
|
-
RemoteDetector,
|
|
1167
|
-
DEFAULT_MODEL as REMOTE_DEFAULT_MODEL,
|
|
1168
|
-
LocalDetector,
|
|
1169
|
-
LABEL_TO_TAG,
|
|
1170
|
-
DEFAULT_BASE_URL,
|
|
1296
|
+
CalibratedDetector,
|
|
1171
1297
|
CompositeDetector,
|
|
1172
|
-
|
|
1298
|
+
CustomPattern,
|
|
1299
|
+
DEFAULT_BASE_URL,
|
|
1300
|
+
LABEL_TO_TAG,
|
|
1301
|
+
LocalDetector,
|
|
1302
|
+
DEFAULT_MODEL as REMOTE_DEFAULT_MODEL,
|
|
1303
|
+
RemoteDetector,
|
|
1304
|
+
SENTINEL_RE,
|
|
1305
|
+
Session,
|
|
1306
|
+
StreamDeanonymizer,
|
|
1307
|
+
VERSION,
|
|
1308
|
+
anonymizeMessages,
|
|
1309
|
+
anonymizeText,
|
|
1310
|
+
deanonymizeStream,
|
|
1311
|
+
deanonymizeText,
|
|
1312
|
+
defaultDetector,
|
|
1313
|
+
isPossibleSentinelPrefix,
|
|
1314
|
+
labelTag,
|
|
1315
|
+
mergeAdjacent,
|
|
1316
|
+
parseSentinel,
|
|
1317
|
+
renderSentinel
|
|
1173
1318
|
};
|
package/dist/esm/openai/index.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined")
|
|
5
|
-
return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
8
|
-
|
|
9
1
|
// src/core/sentinels.ts
|
|
10
2
|
var LABEL_TO_TAG = {
|
|
11
3
|
private_person: "PERSON",
|
|
@@ -164,6 +156,36 @@ function trimSpan(text, start, end) {
|
|
|
164
156
|
return null;
|
|
165
157
|
return { start, end };
|
|
166
158
|
}
|
|
159
|
+
var WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
160
|
+
var isWordChar = (ch) => ch !== undefined && WORD_CHAR.test(ch);
|
|
161
|
+
function snapToWords(text, spans) {
|
|
162
|
+
const snapped = spans.map((sp) => {
|
|
163
|
+
let { start, end } = sp;
|
|
164
|
+
while (start > 0 && isWordChar(text[start - 1]) && isWordChar(text[start]))
|
|
165
|
+
start -= 1;
|
|
166
|
+
while (end < text.length && isWordChar(text[end - 1]) && isWordChar(text[end]))
|
|
167
|
+
end += 1;
|
|
168
|
+
return start === sp.start && end === sp.end ? sp : { ...sp, start, end, text: text.slice(start, end) };
|
|
169
|
+
});
|
|
170
|
+
snapped.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
171
|
+
const merged = [];
|
|
172
|
+
const lastByLabel = new Map;
|
|
173
|
+
for (const sp of snapped) {
|
|
174
|
+
const i = lastByLabel.get(sp.label);
|
|
175
|
+
const prev = i === undefined ? undefined : merged[i];
|
|
176
|
+
if (i !== undefined && prev !== undefined && sp.start <= prev.end) {
|
|
177
|
+
const end = Math.max(prev.end, sp.end);
|
|
178
|
+
const wPrev = prev.end - prev.start;
|
|
179
|
+
const wSp = sp.end - sp.start;
|
|
180
|
+
const score = ((prev.score ?? 1) * wPrev + (sp.score ?? 1) * wSp) / (wPrev + wSp);
|
|
181
|
+
merged[i] = { ...prev, end, text: text.slice(prev.start, end), score };
|
|
182
|
+
} else {
|
|
183
|
+
lastByLabel.set(sp.label, merged.length);
|
|
184
|
+
merged.push(sp);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return merged.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
188
|
+
}
|
|
167
189
|
|
|
168
190
|
// src/detectors/remote.ts
|
|
169
191
|
var DEFAULT_BASE_URL = "https://api.gheim.ch";
|
|
@@ -412,10 +434,7 @@ class LocalDetector {
|
|
|
412
434
|
});
|
|
413
435
|
}
|
|
414
436
|
}
|
|
415
|
-
|
|
416
|
-
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
417
|
-
}
|
|
418
|
-
return out;
|
|
437
|
+
return out.map((spans, i) => snapToWords(texts[i] ?? "", spans));
|
|
419
438
|
}
|
|
420
439
|
async chunkText(text, tokenizer) {
|
|
421
440
|
if (tokenizer) {
|
|
@@ -692,21 +711,136 @@ function maskSpans(text, spans) {
|
|
|
692
711
|
return chars.join("");
|
|
693
712
|
}
|
|
694
713
|
|
|
714
|
+
class CustomPattern {
|
|
715
|
+
label;
|
|
716
|
+
group;
|
|
717
|
+
name;
|
|
718
|
+
regex;
|
|
719
|
+
validator;
|
|
720
|
+
constructor(opts) {
|
|
721
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(opts.label)) {
|
|
722
|
+
throw new Error(`CustomPattern label ${JSON.stringify(opts.label)} must start with a letter and contain ` + "only letters, digits and underscores (it becomes a <TAG_1> placeholder).");
|
|
723
|
+
}
|
|
724
|
+
this.label = opts.label;
|
|
725
|
+
this.name = opts.name ?? opts.label;
|
|
726
|
+
this.group = opts.group ?? 0;
|
|
727
|
+
this.validator = opts.validator;
|
|
728
|
+
const source = typeof opts.regex === "string" ? opts.regex : opts.regex.source;
|
|
729
|
+
const base = typeof opts.regex === "string" ? opts.flags ?? "i" : opts.regex.flags;
|
|
730
|
+
const flags = [...new Set([...base, "g", "d"])].join("");
|
|
731
|
+
this.regex = new RegExp(source, flags);
|
|
732
|
+
const groups = (new RegExp(`${source}|`, flags.replace(/[gd]/g, "")).exec("") ?? []).length - 1;
|
|
733
|
+
if (!Number.isInteger(this.group) || this.group < 0 || this.group > groups) {
|
|
734
|
+
throw new Error(`CustomPattern ${JSON.stringify(this.name)}: group ${this.group} does not exist ` + `(the pattern has ${groups}).`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
find(text) {
|
|
738
|
+
const out = [];
|
|
739
|
+
this.regex.lastIndex = 0;
|
|
740
|
+
let m;
|
|
741
|
+
while ((m = this.regex.exec(text)) !== null) {
|
|
742
|
+
if (m[0] === "") {
|
|
743
|
+
this.regex.lastIndex++;
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
const range = m.indices[this.group];
|
|
747
|
+
if (!range || range[0] === range[1])
|
|
748
|
+
continue;
|
|
749
|
+
const [start, end] = range;
|
|
750
|
+
const value = text.slice(start, end);
|
|
751
|
+
if (this.validator && !this.validator(value))
|
|
752
|
+
continue;
|
|
753
|
+
out.push({ start, end, label: this.label, text: value, score: 1 });
|
|
754
|
+
}
|
|
755
|
+
return out;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
var NAME_PART = /[A-ZÀ-Ý][\p{L}\p{N}_'’-]{3,}/gu;
|
|
759
|
+
var HONORIFICS = new Set([
|
|
760
|
+
"frau",
|
|
761
|
+
"herr",
|
|
762
|
+
"herrn",
|
|
763
|
+
"madame",
|
|
764
|
+
"monsieur",
|
|
765
|
+
"signora",
|
|
766
|
+
"signor",
|
|
767
|
+
"signore",
|
|
768
|
+
"dott",
|
|
769
|
+
"dottor",
|
|
770
|
+
"dottoressa",
|
|
771
|
+
"doktor",
|
|
772
|
+
"docteur",
|
|
773
|
+
"prof",
|
|
774
|
+
"professor",
|
|
775
|
+
"mister",
|
|
776
|
+
"miss",
|
|
777
|
+
"mrs"
|
|
778
|
+
]);
|
|
779
|
+
function escapeRegExp(s) {
|
|
780
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
781
|
+
}
|
|
782
|
+
function propagateNames(text, spans, surnames) {
|
|
783
|
+
const names = new Set;
|
|
784
|
+
for (const sp of spans) {
|
|
785
|
+
if (sp.label !== "private_person")
|
|
786
|
+
continue;
|
|
787
|
+
const value = text.slice(sp.start, sp.end).trim();
|
|
788
|
+
if (value.length >= 4)
|
|
789
|
+
names.add(value);
|
|
790
|
+
if (surnames) {
|
|
791
|
+
for (const part of value.match(NAME_PART) ?? []) {
|
|
792
|
+
if (!HONORIFICS.has(part.toLowerCase()))
|
|
793
|
+
names.add(part);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
let result = [...spans];
|
|
798
|
+
for (const name of [...names].sort((a, b) => b.length - a.length)) {
|
|
799
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegExp(name)}(?![\\p{L}\\p{N}_])`, "gu");
|
|
800
|
+
let m;
|
|
801
|
+
while ((m = re.exec(text)) !== null) {
|
|
802
|
+
const start = m.index;
|
|
803
|
+
const end = start + m[0].length;
|
|
804
|
+
const hits = result.filter((sp) => start < sp.end && sp.start < end);
|
|
805
|
+
if (hits.some((sp) => sp.start <= start && end <= sp.end))
|
|
806
|
+
continue;
|
|
807
|
+
if (hits.some((sp) => sp.start < start || end < sp.end))
|
|
808
|
+
continue;
|
|
809
|
+
result = result.filter((sp) => !hits.includes(sp));
|
|
810
|
+
result.push({ start, end, label: "private_person", text: m[0], score: 1 });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
|
|
695
816
|
class CompositeDetector {
|
|
696
817
|
model;
|
|
818
|
+
patterns;
|
|
819
|
+
propagateNames;
|
|
820
|
+
propagateSurnames;
|
|
697
821
|
constructor(opts) {
|
|
698
822
|
if (!opts || !opts.model) {
|
|
699
823
|
throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
|
|
700
824
|
}
|
|
701
825
|
this.model = opts.model;
|
|
826
|
+
this.patterns = opts.patterns ?? [];
|
|
827
|
+
this.propagateNames = opts.propagateNames ?? true;
|
|
828
|
+
this.propagateSurnames = opts.propagateSurnames ?? false;
|
|
702
829
|
}
|
|
703
830
|
async detect(text, opts) {
|
|
704
831
|
if (!text)
|
|
705
832
|
return [];
|
|
706
833
|
const regexSpans = findRegexSpans(text);
|
|
834
|
+
const extra = this.patterns.flatMap((p) => p.find(text));
|
|
835
|
+
extra.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
836
|
+
for (const sp of extra) {
|
|
837
|
+
if (!regexSpans.some((o) => sp.start < o.end && o.start < sp.end))
|
|
838
|
+
regexSpans.push(sp);
|
|
839
|
+
}
|
|
840
|
+
regexSpans.sort((a, b) => a.start - b.start);
|
|
707
841
|
const masked = maskSpans(text, regexSpans);
|
|
708
842
|
const modelSpans = await this.model.detect(masked, opts);
|
|
709
|
-
|
|
843
|
+
let out = [...regexSpans];
|
|
710
844
|
const regexRanges = regexSpans.map((s) => [s.start, s.end]);
|
|
711
845
|
for (const sp of modelSpans) {
|
|
712
846
|
const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
|
|
@@ -714,6 +848,8 @@ class CompositeDetector {
|
|
|
714
848
|
continue;
|
|
715
849
|
out.push(sp);
|
|
716
850
|
}
|
|
851
|
+
if (this.propagateNames)
|
|
852
|
+
out = propagateNames(text, out, this.propagateSurnames);
|
|
717
853
|
out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
718
854
|
return out;
|
|
719
855
|
}
|
|
@@ -803,7 +939,7 @@ class CalibratedDetector {
|
|
|
803
939
|
if (!text)
|
|
804
940
|
return [];
|
|
805
941
|
await this.load();
|
|
806
|
-
return this._detectChunked(text);
|
|
942
|
+
return snapToWords(text, await this._detectChunked(text));
|
|
807
943
|
}
|
|
808
944
|
async _detectChunked(text) {
|
|
809
945
|
if (text.length <= 1500) {
|
|
@@ -870,15 +1006,21 @@ class CalibratedDetector {
|
|
|
870
1006
|
maxIdx = i;
|
|
871
1007
|
}
|
|
872
1008
|
}
|
|
873
|
-
|
|
1009
|
+
let denom = 0;
|
|
1010
|
+
for (let i = 0;i < row.length; i++)
|
|
1011
|
+
denom += Math.exp(row[i] - maxVal);
|
|
1012
|
+
tokenLabels.push({ id, label: this._id2label[maxIdx] ?? "O", prob: 1 / denom });
|
|
874
1013
|
}
|
|
875
1014
|
const spans = [];
|
|
876
1015
|
let cursor = 0;
|
|
877
1016
|
let groupIds = [];
|
|
1017
|
+
let groupProbs = [];
|
|
878
1018
|
let groupCat = null;
|
|
879
1019
|
const flush = () => {
|
|
880
1020
|
if (groupCat === null || groupIds.length === 0)
|
|
881
1021
|
return;
|
|
1022
|
+
const score = groupProbs.reduce((a, b) => a + b, 0) / groupProbs.length;
|
|
1023
|
+
groupProbs = [];
|
|
882
1024
|
const surface = normalizeDecoded(tokenizer.decode(groupIds, { skip_special_tokens: true }));
|
|
883
1025
|
groupIds = [];
|
|
884
1026
|
const cat = groupCat;
|
|
@@ -900,17 +1042,19 @@ class CalibratedDetector {
|
|
|
900
1042
|
end: e + charOffset,
|
|
901
1043
|
label: cat,
|
|
902
1044
|
text: text.slice(s, e),
|
|
903
|
-
score
|
|
1045
|
+
score
|
|
904
1046
|
});
|
|
905
1047
|
};
|
|
906
|
-
for (const { id, label } of tokenLabels) {
|
|
1048
|
+
for (const { id, label, prob } of tokenLabels) {
|
|
907
1049
|
const cat = label === "O" ? null : label.split("-", 2)[1] ?? null;
|
|
908
1050
|
if (cat === groupCat && cat !== null) {
|
|
909
1051
|
groupIds.push(id);
|
|
1052
|
+
groupProbs.push(prob);
|
|
910
1053
|
} else {
|
|
911
1054
|
flush();
|
|
912
1055
|
if (cat !== null) {
|
|
913
1056
|
groupIds.push(id);
|
|
1057
|
+
groupProbs.push(prob);
|
|
914
1058
|
groupCat = cat;
|
|
915
1059
|
}
|
|
916
1060
|
}
|
|
@@ -996,7 +1140,7 @@ function readLogits(tensor) {
|
|
|
996
1140
|
// src/detectors/index.ts
|
|
997
1141
|
function defaultDetector() {
|
|
998
1142
|
const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
|
|
999
|
-
return hasKey ? new RemoteDetector : new CalibratedDetector;
|
|
1143
|
+
return hasKey ? new RemoteDetector : new CompositeDetector({ model: new CalibratedDetector });
|
|
1000
1144
|
}
|
|
1001
1145
|
|
|
1002
1146
|
// src/core/session.ts
|
|
@@ -1775,11 +1919,11 @@ async function* deanonymizeOpenAIStream(stream, session) {
|
|
|
1775
1919
|
}
|
|
1776
1920
|
}
|
|
1777
1921
|
export {
|
|
1778
|
-
|
|
1779
|
-
deanonymizeOpenAIResponse,
|
|
1780
|
-
anonymizeOpenAIMessages,
|
|
1781
|
-
STRICT_REASONS,
|
|
1782
|
-
OpenAI,
|
|
1922
|
+
AsyncOpenAI,
|
|
1783
1923
|
GheimStrictError,
|
|
1784
|
-
|
|
1924
|
+
OpenAI,
|
|
1925
|
+
STRICT_REASONS,
|
|
1926
|
+
anonymizeOpenAIMessages,
|
|
1927
|
+
deanonymizeOpenAIResponse,
|
|
1928
|
+
deanonymizeOpenAIStream
|
|
1785
1929
|
};
|
|
@@ -25,4 +25,17 @@ export interface Detector {
|
|
|
25
25
|
model?: string;
|
|
26
26
|
}): Promise<Span[]>;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Widen spans that start or end inside a word to the whole word, then merge
|
|
30
|
+
* same-label spans that overlap or touch.
|
|
31
|
+
*
|
|
32
|
+
* A token classifier sometimes tags only part of a word (`gano` of `Lugano`,
|
|
33
|
+
* `Zbind` + `en` of `Zbinden`), and a window boundary can cut a word in half.
|
|
34
|
+
* Masking a fragment leaves the rest of the word readable and splits one entity
|
|
35
|
+
* into two placeholders. A span is widened only when the characters on both
|
|
36
|
+
* sides of its edge are word characters, so edges at spaces or punctuation
|
|
37
|
+
* (`Gay` in `Gay-Crosier`) are left alone. The merged span's score is the
|
|
38
|
+
* length-weighted mean of its parts.
|
|
39
|
+
*/
|
|
40
|
+
export declare function snapToWords(text: string, spans: Span[]): Span[];
|
|
28
41
|
//# sourceMappingURL=base.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../src/detectors/base.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAEhE;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKvC;AAED,MAAM,WAAW,IAAI;IACnB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;CAClE"}
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../../src/detectors/base.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAEhE;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKvC;AAED,MAAM,WAAW,IAAI;IACnB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;CAClE;AAKD;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,EAAE,CA0B/D"}
|
|
@@ -1,31 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Calibrated detector — subtracts a constant `oBias` from the O-class
|
|
3
|
-
* logit before argmax. Originally introduced to recover the v1 model's
|
|
4
|
-
* "single-category mode" pathology (Müller + IBAN co-occurrence flipped
|
|
5
|
-
* the person prediction to O); the v3 / gheim-ch-560m training pipeline
|
|
6
|
-
* largely resolved that pathology, but the calibration still buys a
|
|
7
|
-
* small precision/recall improvement and remains the default.
|
|
8
|
-
*
|
|
9
|
-
* Sweep on gheim-ch-560m (see eval/calibration_sweep.json):
|
|
10
|
-
* `oBias=0.5` is the Pareto-clean default — probe perfect-rate is flat
|
|
11
|
-
* at 91.5% across bias 0.0 and 0.5, and test char-F1 lifts by 0.27pp.
|
|
12
|
-
* Higher biases (1.0–1.5) trade test F1 for marginal probe gains.
|
|
13
|
-
*
|
|
14
|
-
* A separate sweep on the int8 ONNX path via `transformers.js`
|
|
15
|
-
* (eval/calibration_sweep_q8.json) showed a much larger calibration
|
|
16
|
-
* effect (+1.4pp probe, +0.43pp char-F1 at 0.5). That gap is not a
|
|
17
|
-
* property of the int8 quantisation — Python `onnxruntime` on the same
|
|
18
|
-
* int8 file gives 91.0% probe perfect-rate, essentially fp32-equivalent
|
|
19
|
-
* — but of a runtime divergence inside `onnxruntime-web` / WASM that
|
|
20
|
-
* the calibration partially papers over. The reliable fix is to use the
|
|
21
|
-
* `model_fp16.onnx` variant when WebGPU is available (which is what
|
|
22
|
-
* LocalDetector's default `dtype: "auto"` does); int8 should be treated
|
|
23
|
-
* as a low-RAM fallback. See eval/q8_quality_report.md for the
|
|
24
|
-
* diagnostic.
|
|
25
|
-
*
|
|
26
|
-
* `defaultDetector()` returns this with oBias=0.5, so users get the
|
|
27
|
-
* better behavior automatically.
|
|
28
|
-
*/
|
|
29
1
|
import type { Detector, Span } from "./base.ts";
|
|
30
2
|
import type { LocalDetectorLoadEvent } from "./local.ts";
|
|
31
3
|
/** Constructor options for {@link CalibratedDetector}. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calibrated.d.ts","sourceRoot":"","sources":["../../../src/detectors/calibrated.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"calibrated.d.ts","sourceRoot":"","sources":["../../../src/detectors/calibrated.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAMzD,0DAA0D;AAC1D,MAAM,WAAW,yBAAyB;IACxC,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACtD;AA4CD,uDAAuD;AACvD,qBAAa,kBAAmB,YAAW,QAAQ;IACjD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,oEAAoE;IACpE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IACnC;sEACkE;IAClE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAClC,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,SAAS,CAA8B;IAC/C,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC,CAA0C;gBAE3D,IAAI,GAAE,yBAA8B;IAiB1C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoDrB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;YAYxD,cAAc;YA0Bd,aAAa;IAuG3B,OAAO,CAAC,aAAa;CAiBtB"}
|