@onco-foundry/mask-port 0.1.4 → 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.
@@ -37,6 +37,6 @@ export type TextInMaskerOptions = {
37
37
  export declare const findSensitiveWordsByRules: (text: string) => readonly SensitiveWord[];
38
38
  /**
39
39
  * 创建 TextIn 识别脱敏器。凭证在 create 时一次绑定,之后 mask 只传业务参数。
40
- * 同一敏感词在一行出现多次时逐处遮盖、逐处登记 mapping(占位符按类别各自编号)。
40
+ * 同一敏感词出现多次时逐处遮盖、逐处登记 mapping;跨行命中拆成多个框,仍登记为一个实体。
41
41
  */
42
42
  export declare const createTextInMasker: (options: TextInMaskerOptions) => Masker;
@@ -82,9 +82,39 @@ const locateWordQuad = (line, beginIndex, wordLength) => {
82
82
  return line.position === undefined ? undefined : toQuad(line.position);
83
83
  };
84
84
  const scaleQuadToImage = (quad, scaleX, scaleY) => quad.map(([x, y]) => [x * scaleX, y * scaleY]);
85
+ /**
86
+ * Find exact occurrences against the same newline-joined text seen by the finder,
87
+ * then split each occurrence into line-local spans for geometry lookup.
88
+ */
89
+ const locateWordSpans = (lines, recognizedText, word) => {
90
+ const lineRanges = [];
91
+ let offset = 0;
92
+ for (const line of lines) {
93
+ lineRanges.push({ line, start: offset, end: offset + line.text.length });
94
+ offset += line.text.length + 1;
95
+ }
96
+ const occurrences = [];
97
+ let fromIndex = 0;
98
+ while (word.length > 0) {
99
+ const hit = recognizedText.indexOf(word, fromIndex);
100
+ if (hit < 0)
101
+ break;
102
+ fromIndex = hit + word.length;
103
+ const end = hit + word.length;
104
+ const spans = lineRanges.flatMap(range => {
105
+ const spanStart = Math.max(hit, range.start);
106
+ const spanEnd = Math.min(end, range.end);
107
+ return spanStart < spanEnd
108
+ ? [{ line: range.line, beginIndex: spanStart - range.start, length: spanEnd - spanStart }]
109
+ : [];
110
+ });
111
+ occurrences.push(spans);
112
+ }
113
+ return occurrences;
114
+ };
85
115
  /**
86
116
  * 创建 TextIn 识别脱敏器。凭证在 create 时一次绑定,之后 mask 只传业务参数。
87
- * 同一敏感词在一行出现多次时逐处遮盖、逐处登记 mapping(占位符按类别各自编号)。
117
+ * 同一敏感词出现多次时逐处遮盖、逐处登记 mapping;跨行命中拆成多个框,仍登记为一个实体。
88
118
  */
89
119
  export const createTextInMasker = (options) => {
90
120
  const credentials = z.object({
@@ -178,20 +208,18 @@ export const createTextInMasker = (options) => {
178
208
  const counters = new Map();
179
209
  for (const word of requested) {
180
210
  let located = 0;
181
- for (const line of lines) {
182
- let fromIndex = 0;
183
- while (true) {
184
- const hit = line.text.indexOf(word.text, fromIndex);
185
- if (hit < 0)
186
- break;
187
- fromIndex = hit + word.text.length;
188
- const locatedLine = options.requireAllMatches && line.char_positions?.length !== line.text.length
189
- ? { ...line, char_positions: undefined } : line;
190
- const quad = locateWordQuad(locatedLine, hit, word.text.length);
211
+ for (const spans of locateWordSpans(lines, recognizedText, word.text)) {
212
+ const occurrenceQuads = [];
213
+ let complete = spans.length > 0;
214
+ for (const span of spans) {
215
+ const locatedLine = options.requireAllMatches && span.line.char_positions?.length !== span.line.text.length
216
+ ? { ...span.line, char_positions: undefined } : span.line;
217
+ const quad = locateWordQuad(locatedLine, span.beginIndex, span.length);
191
218
  if (quad === undefined) {
192
219
  if (options.requireAllMatches)
193
220
  throw new AppError('TextIn 脱敏敏感词缺少坐标', 502);
194
- continue;
221
+ complete = false;
222
+ break;
195
223
  }
196
224
  if (options.requireAllMatches) {
197
225
  const scaled = scaleQuadToImage(quad, scaleX, scaleY);
@@ -208,8 +236,11 @@ export const createTextInMasker = (options) => {
208
236
  throw new AppError('TextIn 脱敏坐标无效', 502);
209
237
  }
210
238
  }
239
+ occurrenceQuads.push(clampQuad(scaleQuad(scaleQuadToImage(quad, scaleX, scaleY), SAFETY_SCALE), imageWidth, imageHeight));
240
+ }
241
+ if (complete && occurrenceQuads.length > 0) {
211
242
  located++;
212
- finalQuads.push(clampQuad(scaleQuad(scaleQuadToImage(quad, scaleX, scaleY), SAFETY_SCALE), imageWidth, imageHeight));
243
+ finalQuads.push(...occurrenceQuads);
213
244
  const count = (counters.get(word.target) ?? 0) + 1;
214
245
  counters.set(word.target, count);
215
246
  mappings.push({
@@ -220,7 +251,7 @@ export const createTextInMasker = (options) => {
220
251
  }
221
252
  }
222
253
  if (options.requireAllMatches && located === 0) {
223
- throw new AppError('TextIn 脱敏敏感词无法在单行定位', 502);
254
+ throw new AppError('TextIn 脱敏敏感词无法定位', 502);
224
255
  }
225
256
  }
226
257
  const overlay = polygonSvg(imageWidth, imageHeight, finalQuads);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onco-foundry/mask-port",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -16,8 +16,8 @@
16
16
  "agent-lattice": "0.25.0",
17
17
  "sharp": "^0.35.3",
18
18
  "zod": "^4.4.3",
19
- "@onco-foundry/capability-registry": "0.1.1",
20
19
  "@onco-foundry/errors": "0.1.1",
20
+ "@onco-foundry/capability-registry": "0.1.1",
21
21
  "@onco-foundry/trace-port": "0.3.2",
22
22
  "@onco-foundry/resource-versioning": "0.3.0"
23
23
  },