gheim 0.1.8 → 0.1.10
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 +184 -31
- package/dist/cjs/openai/index.cjs +169 -17
- package/dist/esm/index.js +184 -39
- package/dist/esm/openai/index.js +169 -25
- 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/detectors/remote.d.ts +1 -1
- package/dist/types/detectors/remote.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
|
@@ -39,13 +39,13 @@ var __export = (target, all) => {
|
|
|
39
39
|
// src/openai/index.ts
|
|
40
40
|
var exports_openai = {};
|
|
41
41
|
__export(exports_openai, {
|
|
42
|
-
|
|
43
|
-
deanonymizeOpenAIResponse: () => deanonymizeOpenAIResponse,
|
|
44
|
-
anonymizeOpenAIMessages: () => anonymizeOpenAIMessages,
|
|
45
|
-
STRICT_REASONS: () => STRICT_REASONS,
|
|
46
|
-
OpenAI: () => OpenAI,
|
|
42
|
+
AsyncOpenAI: () => AsyncOpenAI,
|
|
47
43
|
GheimStrictError: () => GheimStrictError,
|
|
48
|
-
|
|
44
|
+
OpenAI: () => OpenAI,
|
|
45
|
+
STRICT_REASONS: () => STRICT_REASONS,
|
|
46
|
+
anonymizeOpenAIMessages: () => anonymizeOpenAIMessages,
|
|
47
|
+
deanonymizeOpenAIResponse: () => deanonymizeOpenAIResponse,
|
|
48
|
+
deanonymizeOpenAIStream: () => deanonymizeOpenAIStream
|
|
49
49
|
});
|
|
50
50
|
module.exports = __toCommonJS(exports_openai);
|
|
51
51
|
|
|
@@ -207,10 +207,40 @@ function trimSpan(text, start, end) {
|
|
|
207
207
|
return null;
|
|
208
208
|
return { start, end };
|
|
209
209
|
}
|
|
210
|
+
var WORD_CHAR = /[\p{L}\p{N}_]/u;
|
|
211
|
+
var isWordChar = (ch) => ch !== undefined && WORD_CHAR.test(ch);
|
|
212
|
+
function snapToWords(text, spans) {
|
|
213
|
+
const snapped = spans.map((sp) => {
|
|
214
|
+
let { start, end } = sp;
|
|
215
|
+
while (start > 0 && isWordChar(text[start - 1]) && isWordChar(text[start]))
|
|
216
|
+
start -= 1;
|
|
217
|
+
while (end < text.length && isWordChar(text[end - 1]) && isWordChar(text[end]))
|
|
218
|
+
end += 1;
|
|
219
|
+
return start === sp.start && end === sp.end ? sp : { ...sp, start, end, text: text.slice(start, end) };
|
|
220
|
+
});
|
|
221
|
+
snapped.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
222
|
+
const merged = [];
|
|
223
|
+
const lastByLabel = new Map;
|
|
224
|
+
for (const sp of snapped) {
|
|
225
|
+
const i = lastByLabel.get(sp.label);
|
|
226
|
+
const prev = i === undefined ? undefined : merged[i];
|
|
227
|
+
if (i !== undefined && prev !== undefined && sp.start <= prev.end) {
|
|
228
|
+
const end = Math.max(prev.end, sp.end);
|
|
229
|
+
const wPrev = prev.end - prev.start;
|
|
230
|
+
const wSp = sp.end - sp.start;
|
|
231
|
+
const score = ((prev.score ?? 1) * wPrev + (sp.score ?? 1) * wSp) / (wPrev + wSp);
|
|
232
|
+
merged[i] = { ...prev, end, text: text.slice(prev.start, end), score };
|
|
233
|
+
} else {
|
|
234
|
+
lastByLabel.set(sp.label, merged.length);
|
|
235
|
+
merged.push(sp);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return merged.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
239
|
+
}
|
|
210
240
|
|
|
211
241
|
// src/detectors/remote.ts
|
|
212
242
|
var DEFAULT_BASE_URL = "https://api.gheim.ch";
|
|
213
|
-
var DEFAULT_MODEL = "
|
|
243
|
+
var DEFAULT_MODEL = "joelbarmettler/gheim-ch-560m";
|
|
214
244
|
|
|
215
245
|
class RemoteDetector {
|
|
216
246
|
baseUrl;
|
|
@@ -455,10 +485,7 @@ class LocalDetector {
|
|
|
455
485
|
});
|
|
456
486
|
}
|
|
457
487
|
}
|
|
458
|
-
|
|
459
|
-
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
460
|
-
}
|
|
461
|
-
return out;
|
|
488
|
+
return out.map((spans, i) => snapToWords(texts[i] ?? "", spans));
|
|
462
489
|
}
|
|
463
490
|
async chunkText(text, tokenizer) {
|
|
464
491
|
if (tokenizer) {
|
|
@@ -735,21 +762,136 @@ function maskSpans(text, spans) {
|
|
|
735
762
|
return chars.join("");
|
|
736
763
|
}
|
|
737
764
|
|
|
765
|
+
class CustomPattern {
|
|
766
|
+
label;
|
|
767
|
+
group;
|
|
768
|
+
name;
|
|
769
|
+
regex;
|
|
770
|
+
validator;
|
|
771
|
+
constructor(opts) {
|
|
772
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(opts.label)) {
|
|
773
|
+
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).");
|
|
774
|
+
}
|
|
775
|
+
this.label = opts.label;
|
|
776
|
+
this.name = opts.name ?? opts.label;
|
|
777
|
+
this.group = opts.group ?? 0;
|
|
778
|
+
this.validator = opts.validator;
|
|
779
|
+
const source = typeof opts.regex === "string" ? opts.regex : opts.regex.source;
|
|
780
|
+
const base = typeof opts.regex === "string" ? opts.flags ?? "i" : opts.regex.flags;
|
|
781
|
+
const flags = [...new Set([...base, "g", "d"])].join("");
|
|
782
|
+
this.regex = new RegExp(source, flags);
|
|
783
|
+
const groups = (new RegExp(`${source}|`, flags.replace(/[gd]/g, "")).exec("") ?? []).length - 1;
|
|
784
|
+
if (!Number.isInteger(this.group) || this.group < 0 || this.group > groups) {
|
|
785
|
+
throw new Error(`CustomPattern ${JSON.stringify(this.name)}: group ${this.group} does not exist ` + `(the pattern has ${groups}).`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
find(text) {
|
|
789
|
+
const out = [];
|
|
790
|
+
this.regex.lastIndex = 0;
|
|
791
|
+
let m;
|
|
792
|
+
while ((m = this.regex.exec(text)) !== null) {
|
|
793
|
+
if (m[0] === "") {
|
|
794
|
+
this.regex.lastIndex++;
|
|
795
|
+
continue;
|
|
796
|
+
}
|
|
797
|
+
const range = m.indices[this.group];
|
|
798
|
+
if (!range || range[0] === range[1])
|
|
799
|
+
continue;
|
|
800
|
+
const [start, end] = range;
|
|
801
|
+
const value = text.slice(start, end);
|
|
802
|
+
if (this.validator && !this.validator(value))
|
|
803
|
+
continue;
|
|
804
|
+
out.push({ start, end, label: this.label, text: value, score: 1 });
|
|
805
|
+
}
|
|
806
|
+
return out;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
var NAME_PART = /[A-ZÀ-Ý][\p{L}\p{N}_'’-]{3,}/gu;
|
|
810
|
+
var HONORIFICS = new Set([
|
|
811
|
+
"frau",
|
|
812
|
+
"herr",
|
|
813
|
+
"herrn",
|
|
814
|
+
"madame",
|
|
815
|
+
"monsieur",
|
|
816
|
+
"signora",
|
|
817
|
+
"signor",
|
|
818
|
+
"signore",
|
|
819
|
+
"dott",
|
|
820
|
+
"dottor",
|
|
821
|
+
"dottoressa",
|
|
822
|
+
"doktor",
|
|
823
|
+
"docteur",
|
|
824
|
+
"prof",
|
|
825
|
+
"professor",
|
|
826
|
+
"mister",
|
|
827
|
+
"miss",
|
|
828
|
+
"mrs"
|
|
829
|
+
]);
|
|
830
|
+
function escapeRegExp(s) {
|
|
831
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
832
|
+
}
|
|
833
|
+
function propagateNames(text, spans, surnames) {
|
|
834
|
+
const names = new Set;
|
|
835
|
+
for (const sp of spans) {
|
|
836
|
+
if (sp.label !== "private_person")
|
|
837
|
+
continue;
|
|
838
|
+
const value = text.slice(sp.start, sp.end).trim();
|
|
839
|
+
if (value.length >= 4)
|
|
840
|
+
names.add(value);
|
|
841
|
+
if (surnames) {
|
|
842
|
+
for (const part of value.match(NAME_PART) ?? []) {
|
|
843
|
+
if (!HONORIFICS.has(part.toLowerCase()))
|
|
844
|
+
names.add(part);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
let result = [...spans];
|
|
849
|
+
for (const name of [...names].sort((a, b) => b.length - a.length)) {
|
|
850
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}_])${escapeRegExp(name)}(?![\\p{L}\\p{N}_])`, "gu");
|
|
851
|
+
let m;
|
|
852
|
+
while ((m = re.exec(text)) !== null) {
|
|
853
|
+
const start = m.index;
|
|
854
|
+
const end = start + m[0].length;
|
|
855
|
+
const hits = result.filter((sp) => start < sp.end && sp.start < end);
|
|
856
|
+
if (hits.some((sp) => sp.start <= start && end <= sp.end))
|
|
857
|
+
continue;
|
|
858
|
+
if (hits.some((sp) => sp.start < start || end < sp.end))
|
|
859
|
+
continue;
|
|
860
|
+
result = result.filter((sp) => !hits.includes(sp));
|
|
861
|
+
result.push({ start, end, label: "private_person", text: m[0], score: 1 });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return result;
|
|
865
|
+
}
|
|
866
|
+
|
|
738
867
|
class CompositeDetector {
|
|
739
868
|
model;
|
|
869
|
+
patterns;
|
|
870
|
+
propagateNames;
|
|
871
|
+
propagateSurnames;
|
|
740
872
|
constructor(opts) {
|
|
741
873
|
if (!opts || !opts.model) {
|
|
742
874
|
throw new Error("CompositeDetector requires { model: Detector } — pass any object with " + "a .detect(text) method (e.g. new LocalDetector({...})).");
|
|
743
875
|
}
|
|
744
876
|
this.model = opts.model;
|
|
877
|
+
this.patterns = opts.patterns ?? [];
|
|
878
|
+
this.propagateNames = opts.propagateNames ?? true;
|
|
879
|
+
this.propagateSurnames = opts.propagateSurnames ?? false;
|
|
745
880
|
}
|
|
746
881
|
async detect(text, opts) {
|
|
747
882
|
if (!text)
|
|
748
883
|
return [];
|
|
749
884
|
const regexSpans = findRegexSpans(text);
|
|
885
|
+
const extra = this.patterns.flatMap((p) => p.find(text));
|
|
886
|
+
extra.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
887
|
+
for (const sp of extra) {
|
|
888
|
+
if (!regexSpans.some((o) => sp.start < o.end && o.start < sp.end))
|
|
889
|
+
regexSpans.push(sp);
|
|
890
|
+
}
|
|
891
|
+
regexSpans.sort((a, b) => a.start - b.start);
|
|
750
892
|
const masked = maskSpans(text, regexSpans);
|
|
751
893
|
const modelSpans = await this.model.detect(masked, opts);
|
|
752
|
-
|
|
894
|
+
let out = [...regexSpans];
|
|
753
895
|
const regexRanges = regexSpans.map((s) => [s.start, s.end]);
|
|
754
896
|
for (const sp of modelSpans) {
|
|
755
897
|
const overlaps = regexRanges.some(([rs, re]) => sp.start < re && rs < sp.end);
|
|
@@ -757,6 +899,8 @@ class CompositeDetector {
|
|
|
757
899
|
continue;
|
|
758
900
|
out.push(sp);
|
|
759
901
|
}
|
|
902
|
+
if (this.propagateNames)
|
|
903
|
+
out = propagateNames(text, out, this.propagateSurnames);
|
|
760
904
|
out.sort((a, b) => a.start !== b.start ? a.start - b.start : b.end - b.start - (a.end - a.start));
|
|
761
905
|
return out;
|
|
762
906
|
}
|
|
@@ -846,7 +990,7 @@ class CalibratedDetector {
|
|
|
846
990
|
if (!text)
|
|
847
991
|
return [];
|
|
848
992
|
await this.load();
|
|
849
|
-
return this._detectChunked(text);
|
|
993
|
+
return snapToWords(text, await this._detectChunked(text));
|
|
850
994
|
}
|
|
851
995
|
async _detectChunked(text) {
|
|
852
996
|
if (text.length <= 1500) {
|
|
@@ -913,15 +1057,21 @@ class CalibratedDetector {
|
|
|
913
1057
|
maxIdx = i;
|
|
914
1058
|
}
|
|
915
1059
|
}
|
|
916
|
-
|
|
1060
|
+
let denom = 0;
|
|
1061
|
+
for (let i = 0;i < row.length; i++)
|
|
1062
|
+
denom += Math.exp(row[i] - maxVal);
|
|
1063
|
+
tokenLabels.push({ id, label: this._id2label[maxIdx] ?? "O", prob: 1 / denom });
|
|
917
1064
|
}
|
|
918
1065
|
const spans = [];
|
|
919
1066
|
let cursor = 0;
|
|
920
1067
|
let groupIds = [];
|
|
1068
|
+
let groupProbs = [];
|
|
921
1069
|
let groupCat = null;
|
|
922
1070
|
const flush = () => {
|
|
923
1071
|
if (groupCat === null || groupIds.length === 0)
|
|
924
1072
|
return;
|
|
1073
|
+
const score = groupProbs.reduce((a, b) => a + b, 0) / groupProbs.length;
|
|
1074
|
+
groupProbs = [];
|
|
925
1075
|
const surface = normalizeDecoded(tokenizer.decode(groupIds, { skip_special_tokens: true }));
|
|
926
1076
|
groupIds = [];
|
|
927
1077
|
const cat = groupCat;
|
|
@@ -943,17 +1093,19 @@ class CalibratedDetector {
|
|
|
943
1093
|
end: e + charOffset,
|
|
944
1094
|
label: cat,
|
|
945
1095
|
text: text.slice(s, e),
|
|
946
|
-
score
|
|
1096
|
+
score
|
|
947
1097
|
});
|
|
948
1098
|
};
|
|
949
|
-
for (const { id, label } of tokenLabels) {
|
|
1099
|
+
for (const { id, label, prob } of tokenLabels) {
|
|
950
1100
|
const cat = label === "O" ? null : label.split("-", 2)[1] ?? null;
|
|
951
1101
|
if (cat === groupCat && cat !== null) {
|
|
952
1102
|
groupIds.push(id);
|
|
1103
|
+
groupProbs.push(prob);
|
|
953
1104
|
} else {
|
|
954
1105
|
flush();
|
|
955
1106
|
if (cat !== null) {
|
|
956
1107
|
groupIds.push(id);
|
|
1108
|
+
groupProbs.push(prob);
|
|
957
1109
|
groupCat = cat;
|
|
958
1110
|
}
|
|
959
1111
|
}
|
|
@@ -1039,7 +1191,7 @@ function readLogits(tensor) {
|
|
|
1039
1191
|
// src/detectors/index.ts
|
|
1040
1192
|
function defaultDetector() {
|
|
1041
1193
|
const hasKey = typeof process !== "undefined" && Boolean(process.env?.GHEIM_API_KEY);
|
|
1042
|
-
return hasKey ? new RemoteDetector : new CalibratedDetector;
|
|
1194
|
+
return hasKey ? new RemoteDetector : new CompositeDetector({ model: new CalibratedDetector });
|
|
1043
1195
|
}
|
|
1044
1196
|
|
|
1045
1197
|
// src/core/session.ts
|
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,10 +256,40 @@ 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";
|
|
270
|
-
var DEFAULT_MODEL = "
|
|
292
|
+
var DEFAULT_MODEL = "joelbarmettler/gheim-ch-560m";
|
|
271
293
|
|
|
272
294
|
class RemoteDetector {
|
|
273
295
|
baseUrl;
|
|
@@ -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
|
};
|