@precisa-saude/fhir-ocr-utils 0.31.3 → 0.31.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/cli.js +156 -31
- package/dist/index.cjs +157 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +155 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -20,13 +20,8 @@ import {
|
|
|
20
20
|
getAllSearchPatterns,
|
|
21
21
|
UNIT_TO_UCUM
|
|
22
22
|
} from "@precisa-saude/fhir";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
var CONFIDENCE_AMBIGUOUS = 0.4;
|
|
26
|
-
var MAX_OCCURRENCES_PER_NAME = 5;
|
|
27
|
-
function normalize(text) {
|
|
28
|
-
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/(?<=\p{L})-(?=[\p{L}\p{N}])/gu, " ").replace(/[^\S\n]+/g, " ");
|
|
29
|
-
}
|
|
23
|
+
|
|
24
|
+
// src/anchor-lexicon.ts
|
|
30
25
|
var UNAMBIGUOUS_SHORT_NAMES = /* @__PURE__ */ new Set([
|
|
31
26
|
"hdl",
|
|
32
27
|
"ldl",
|
|
@@ -140,6 +135,85 @@ var QUALITATIVE_VALUE_TERMS = /* @__PURE__ */ new Set([
|
|
|
140
135
|
"undetectable",
|
|
141
136
|
"yellow"
|
|
142
137
|
]);
|
|
138
|
+
|
|
139
|
+
// src/body-region.ts
|
|
140
|
+
var WHOLE_BODY_COMPOSITION_CODES = /* @__PURE__ */ new Set([
|
|
141
|
+
"BMC",
|
|
142
|
+
"BodyFatPct",
|
|
143
|
+
"FatFreeMass",
|
|
144
|
+
"FatMass",
|
|
145
|
+
"LeanMass",
|
|
146
|
+
"TotalMass"
|
|
147
|
+
]);
|
|
148
|
+
var LIMB_PAIR_CODES = /* @__PURE__ */ new Set(["ArmsFatMass", "ArmsLeanMass", "LegsFatMass", "LegsLeanMass"]);
|
|
149
|
+
var BODY_REGION = String.raw`(?:arms?|legs?|trunk|head|android|gynoid|bracos?|pernas?|tronco|cabeca|androide|ginoide)`;
|
|
150
|
+
var BODY_SIDE = String.raw`(?:right|left|direit[oa]|esquerd[oa])`;
|
|
151
|
+
var ENDS_WITH_BODY_REGION = new RegExp(
|
|
152
|
+
String.raw`(?<![\p{L}\p{N}])${BODY_REGION}(?: (?:total|difference|diferenca|${BODY_SIDE}))* ?$`,
|
|
153
|
+
"u"
|
|
154
|
+
);
|
|
155
|
+
var ENDS_WITH_BODY_SIDE = new RegExp(String.raw`(?<![\p{L}\p{N}])${BODY_SIDE} ?$`, "u");
|
|
156
|
+
function qualifiedByBodyRegion(code, before) {
|
|
157
|
+
if (WHOLE_BODY_COMPOSITION_CODES.has(code)) {
|
|
158
|
+
return ENDS_WITH_BODY_REGION.test(before);
|
|
159
|
+
}
|
|
160
|
+
if (LIMB_PAIR_CODES.has(code)) {
|
|
161
|
+
return ENDS_WITH_BODY_SIDE.test(before);
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/anchor.ts
|
|
167
|
+
var CONFIDENCE_VALUE_ADJACENT = 1;
|
|
168
|
+
var CONFIDENCE_NAME_ONLY = 0.7;
|
|
169
|
+
var CONFIDENCE_AMBIGUOUS = 0.4;
|
|
170
|
+
var MAX_OCCURRENCES_PER_NAME = 5;
|
|
171
|
+
function normalizeBase(text) {
|
|
172
|
+
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/(?<=\p{L})-(?=[\p{L}\p{N}])/gu, " ").replace(/[^\S\n]+/g, " ");
|
|
173
|
+
}
|
|
174
|
+
function normalize(text) {
|
|
175
|
+
return foldCommas(normalizeBase(text));
|
|
176
|
+
}
|
|
177
|
+
var cachedNamesByHead = null;
|
|
178
|
+
function isIndexedName(normalized) {
|
|
179
|
+
return normalized.length >= 3 || UNAMBIGUOUS_SHORT_NAMES.has(normalized);
|
|
180
|
+
}
|
|
181
|
+
function getNamesByHead() {
|
|
182
|
+
if (!cachedNamesByHead) {
|
|
183
|
+
const map = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const pattern of getPatterns()) {
|
|
185
|
+
for (const name of pattern.names) {
|
|
186
|
+
const normalized = normalizeBase(name).trim();
|
|
187
|
+
if (!normalized || !isIndexedName(normalized)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const head = normalized.split(" ")[0];
|
|
191
|
+
map.set(head, [...map.get(head) ?? [], normalized]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
cachedNamesByHead = map;
|
|
195
|
+
}
|
|
196
|
+
return cachedNamesByHead;
|
|
197
|
+
}
|
|
198
|
+
function startsWithCatalogName(text) {
|
|
199
|
+
const head = /^[\p{L}\p{N}]+/u.exec(text)?.[0];
|
|
200
|
+
if (!head) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return (getNamesByHead().get(head) ?? []).some((name) => {
|
|
204
|
+
if (!text.startsWith(name)) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
const after = text.slice(name.length).replace(/^s/, "");
|
|
208
|
+
return !/^[\p{L}\p{N}]/u.test(after);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function foldCommas(text) {
|
|
212
|
+
return text.replace(
|
|
213
|
+
/(?<=\p{L}),([^\S\n]*)(?=\p{L})/gu,
|
|
214
|
+
(comma, space, offset) => startsWithCatalogName(text.slice(offset + 1 + space.length)) ? comma : " "
|
|
215
|
+
);
|
|
216
|
+
}
|
|
143
217
|
var GENETIC_CONTEXT_PATTERNS = [
|
|
144
218
|
/\b[nx][mrpc]_\d{6,}/,
|
|
145
219
|
// RefSeq: NM_000384.2, NP_, NR_, XM_
|
|
@@ -233,10 +307,7 @@ function getNamePatterns() {
|
|
|
233
307
|
for (const pattern of getPatterns()) {
|
|
234
308
|
for (const name of pattern.names) {
|
|
235
309
|
const normalized = normalize(name).trim();
|
|
236
|
-
if (!normalized) {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (normalized.length < 3 && !UNAMBIGUOUS_SHORT_NAMES.has(normalized)) {
|
|
310
|
+
if (!normalized || !isIndexedName(normalized)) {
|
|
240
311
|
continue;
|
|
241
312
|
}
|
|
242
313
|
let slot = map.get(normalized);
|
|
@@ -295,6 +366,56 @@ function collectCandidates(normalizedText) {
|
|
|
295
366
|
}
|
|
296
367
|
return candidates;
|
|
297
368
|
}
|
|
369
|
+
function joinLines(text) {
|
|
370
|
+
const parts = [];
|
|
371
|
+
const offsets = [];
|
|
372
|
+
let cursor = 0;
|
|
373
|
+
for (const lineBreak of text.matchAll(/[^\S\n]*\n[^\S\n]*/g)) {
|
|
374
|
+
for (let i = cursor; i < lineBreak.index; i += 1) {
|
|
375
|
+
offsets.push(i);
|
|
376
|
+
}
|
|
377
|
+
parts.push(text.slice(cursor, lineBreak.index), " ");
|
|
378
|
+
offsets.push(lineBreak.index + lineBreak[0].indexOf("\n"));
|
|
379
|
+
cursor = lineBreak.index + lineBreak[0].length;
|
|
380
|
+
}
|
|
381
|
+
for (let i = cursor; i < text.length; i += 1) {
|
|
382
|
+
offsets.push(i);
|
|
383
|
+
}
|
|
384
|
+
parts.push(text.slice(cursor));
|
|
385
|
+
return { joined: parts.join(""), offsets };
|
|
386
|
+
}
|
|
387
|
+
function collectWrappedCandidates(normalizedText) {
|
|
388
|
+
if (!normalizedText.includes("\n")) {
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
const { joined, offsets } = joinLines(normalizedText);
|
|
392
|
+
const namePatterns = getNamePatterns();
|
|
393
|
+
const candidates = [];
|
|
394
|
+
for (const [name, slot] of namePatterns) {
|
|
395
|
+
if (!name.includes(" ") || !joined.includes(name)) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const regex = slot.regex ??= buildNamePattern(name);
|
|
399
|
+
regex.lastIndex = 0;
|
|
400
|
+
let occurrences = 0;
|
|
401
|
+
let match = regex.exec(joined);
|
|
402
|
+
while (match !== null && occurrences < MAX_OCCURRENCES_PER_NAME) {
|
|
403
|
+
const start = offsets[match.index];
|
|
404
|
+
const end = offsets[match.index + match[0].length - 1] + 1;
|
|
405
|
+
const lineBreak = normalizedText.indexOf("\n", start);
|
|
406
|
+
const nextBreak = normalizedText.indexOf("\n", lineBreak + 1);
|
|
407
|
+
const wrapped = lineBreak !== -1 && lineBreak < end && (nextBreak === -1 || nextBreak >= end);
|
|
408
|
+
if (wrapped && !namePatterns.has(normalizedText.slice(start, lineBreak).trim()) && !hasValueEvidence(
|
|
409
|
+
normalizedText.slice(getLineBounds(normalizedText, start).start, lineBreak)
|
|
410
|
+
)) {
|
|
411
|
+
candidates.push({ end, entries: slot.entries, start });
|
|
412
|
+
occurrences += 1;
|
|
413
|
+
}
|
|
414
|
+
match = regex.exec(joined);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return candidates;
|
|
418
|
+
}
|
|
298
419
|
function resolveOverlaps(candidates) {
|
|
299
420
|
const sorted = [...candidates].sort(
|
|
300
421
|
(a, b) => b.end - b.start - (a.end - a.start) || a.start - b.start
|
|
@@ -315,33 +436,37 @@ function findBiomarkersInText(ocrText) {
|
|
|
315
436
|
const startTime = Date.now();
|
|
316
437
|
const normalizedText = normalize(ocrText);
|
|
317
438
|
const bestByCode = /* @__PURE__ */ new Map();
|
|
318
|
-
const
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
439
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
440
|
+
const candidates = [
|
|
441
|
+
...collectCandidates(normalizedText),
|
|
442
|
+
...collectWrappedCandidates(normalizedText)
|
|
443
|
+
];
|
|
444
|
+
for (const candidate of resolveOverlaps(candidates)) {
|
|
445
|
+
const lineStart = getLineBounds(normalizedText, candidate.start).start;
|
|
446
|
+
const lineEnd = getLineBounds(normalizedText, candidate.end - 1).end;
|
|
447
|
+
const key = `${lineStart}:${lineEnd}`;
|
|
448
|
+
let context = contexts.get(key);
|
|
449
|
+
if (!context) {
|
|
450
|
+
const line = normalizedText.slice(lineStart, lineEnd);
|
|
451
|
+
context = {
|
|
452
|
+
genetic: hasGeneticContext(line),
|
|
453
|
+
girth: hasGirthContext(line),
|
|
454
|
+
hasValue: hasValueEvidence(line)
|
|
455
|
+
};
|
|
456
|
+
contexts.set(key, context);
|
|
327
457
|
}
|
|
458
|
+
const { genetic, girth, hasValue } = context;
|
|
328
459
|
if (genetic) {
|
|
329
460
|
continue;
|
|
330
461
|
}
|
|
331
|
-
|
|
332
|
-
if (hasValue === void 0) {
|
|
333
|
-
hasValue = hasValueEvidence(normalizedText.slice(lineStart, lineEnd));
|
|
334
|
-
valueLines.set(lineStart, hasValue);
|
|
335
|
-
}
|
|
336
|
-
let girth = girthLines.get(lineStart);
|
|
337
|
-
if (girth === void 0) {
|
|
338
|
-
girth = hasGirthContext(normalizedText.slice(lineStart, lineEnd));
|
|
339
|
-
girthLines.set(lineStart, girth);
|
|
340
|
-
}
|
|
462
|
+
const before = normalizedText.slice(lineStart, candidate.start);
|
|
341
463
|
for (const entry of candidate.entries) {
|
|
342
464
|
if (entry.ambiguous && !hasValue) {
|
|
343
465
|
continue;
|
|
344
466
|
}
|
|
467
|
+
if (qualifiedByBodyRegion(entry.code, before)) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
345
470
|
if (girth && SKINFOLD_SITE_CODES.has(entry.code)) {
|
|
346
471
|
continue;
|
|
347
472
|
}
|
|
@@ -884,7 +1009,7 @@ async function main() {
|
|
|
884
1009
|
strict: false
|
|
885
1010
|
});
|
|
886
1011
|
if (values.version) {
|
|
887
|
-
process.stdout.write(`${"0.31.
|
|
1012
|
+
process.stdout.write(`${"0.31.5"}
|
|
888
1013
|
`);
|
|
889
1014
|
return;
|
|
890
1015
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -4,13 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
var _fhir = require('@precisa-saude/fhir');
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
var CONFIDENCE_AMBIGUOUS = 0.4;
|
|
10
|
-
var MAX_OCCURRENCES_PER_NAME = 5;
|
|
11
|
-
function normalize(text) {
|
|
12
|
-
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/(?<=\p{L})-(?=[\p{L}\p{N}])/gu, " ").replace(/[^\S\n]+/g, " ");
|
|
13
|
-
}
|
|
7
|
+
|
|
8
|
+
// src/anchor-lexicon.ts
|
|
14
9
|
var UNAMBIGUOUS_SHORT_NAMES = /* @__PURE__ */ new Set([
|
|
15
10
|
"hdl",
|
|
16
11
|
"ldl",
|
|
@@ -124,6 +119,85 @@ var QUALITATIVE_VALUE_TERMS = /* @__PURE__ */ new Set([
|
|
|
124
119
|
"undetectable",
|
|
125
120
|
"yellow"
|
|
126
121
|
]);
|
|
122
|
+
|
|
123
|
+
// src/body-region.ts
|
|
124
|
+
var WHOLE_BODY_COMPOSITION_CODES = /* @__PURE__ */ new Set([
|
|
125
|
+
"BMC",
|
|
126
|
+
"BodyFatPct",
|
|
127
|
+
"FatFreeMass",
|
|
128
|
+
"FatMass",
|
|
129
|
+
"LeanMass",
|
|
130
|
+
"TotalMass"
|
|
131
|
+
]);
|
|
132
|
+
var LIMB_PAIR_CODES = /* @__PURE__ */ new Set(["ArmsFatMass", "ArmsLeanMass", "LegsFatMass", "LegsLeanMass"]);
|
|
133
|
+
var BODY_REGION = String.raw`(?:arms?|legs?|trunk|head|android|gynoid|bracos?|pernas?|tronco|cabeca|androide|ginoide)`;
|
|
134
|
+
var BODY_SIDE = String.raw`(?:right|left|direit[oa]|esquerd[oa])`;
|
|
135
|
+
var ENDS_WITH_BODY_REGION = new RegExp(
|
|
136
|
+
String.raw`(?<![\p{L}\p{N}])${BODY_REGION}(?: (?:total|difference|diferenca|${BODY_SIDE}))* ?$`,
|
|
137
|
+
"u"
|
|
138
|
+
);
|
|
139
|
+
var ENDS_WITH_BODY_SIDE = new RegExp(String.raw`(?<![\p{L}\p{N}])${BODY_SIDE} ?$`, "u");
|
|
140
|
+
function qualifiedByBodyRegion(code, before) {
|
|
141
|
+
if (WHOLE_BODY_COMPOSITION_CODES.has(code)) {
|
|
142
|
+
return ENDS_WITH_BODY_REGION.test(before);
|
|
143
|
+
}
|
|
144
|
+
if (LIMB_PAIR_CODES.has(code)) {
|
|
145
|
+
return ENDS_WITH_BODY_SIDE.test(before);
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/anchor.ts
|
|
151
|
+
var CONFIDENCE_VALUE_ADJACENT = 1;
|
|
152
|
+
var CONFIDENCE_NAME_ONLY = 0.7;
|
|
153
|
+
var CONFIDENCE_AMBIGUOUS = 0.4;
|
|
154
|
+
var MAX_OCCURRENCES_PER_NAME = 5;
|
|
155
|
+
function normalizeBase(text) {
|
|
156
|
+
return text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/(?<=\p{L})-(?=[\p{L}\p{N}])/gu, " ").replace(/[^\S\n]+/g, " ");
|
|
157
|
+
}
|
|
158
|
+
function normalize(text) {
|
|
159
|
+
return foldCommas(normalizeBase(text));
|
|
160
|
+
}
|
|
161
|
+
var cachedNamesByHead = null;
|
|
162
|
+
function isIndexedName(normalized) {
|
|
163
|
+
return normalized.length >= 3 || UNAMBIGUOUS_SHORT_NAMES.has(normalized);
|
|
164
|
+
}
|
|
165
|
+
function getNamesByHead() {
|
|
166
|
+
if (!cachedNamesByHead) {
|
|
167
|
+
const map = /* @__PURE__ */ new Map();
|
|
168
|
+
for (const pattern of getPatterns()) {
|
|
169
|
+
for (const name of pattern.names) {
|
|
170
|
+
const normalized = normalizeBase(name).trim();
|
|
171
|
+
if (!normalized || !isIndexedName(normalized)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const head = normalized.split(" ")[0];
|
|
175
|
+
map.set(head, [..._nullishCoalesce(map.get(head), () => ( [])), normalized]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
cachedNamesByHead = map;
|
|
179
|
+
}
|
|
180
|
+
return cachedNamesByHead;
|
|
181
|
+
}
|
|
182
|
+
function startsWithCatalogName(text) {
|
|
183
|
+
const head = _optionalChain([/^[\p{L}\p{N}]+/u, 'access', _ => _.exec, 'call', _2 => _2(text), 'optionalAccess', _3 => _3[0]]);
|
|
184
|
+
if (!head) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
return (_nullishCoalesce(getNamesByHead().get(head), () => ( []))).some((name) => {
|
|
188
|
+
if (!text.startsWith(name)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const after = text.slice(name.length).replace(/^s/, "");
|
|
192
|
+
return !/^[\p{L}\p{N}]/u.test(after);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
function foldCommas(text) {
|
|
196
|
+
return text.replace(
|
|
197
|
+
/(?<=\p{L}),([^\S\n]*)(?=\p{L})/gu,
|
|
198
|
+
(comma, space, offset) => startsWithCatalogName(text.slice(offset + 1 + space.length)) ? comma : " "
|
|
199
|
+
);
|
|
200
|
+
}
|
|
127
201
|
var GENETIC_CONTEXT_PATTERNS = [
|
|
128
202
|
/\b[nx][mrpc]_\d{6,}/,
|
|
129
203
|
// RefSeq: NM_000384.2, NP_, NR_, XM_
|
|
@@ -217,10 +291,7 @@ function getNamePatterns() {
|
|
|
217
291
|
for (const pattern of getPatterns()) {
|
|
218
292
|
for (const name of pattern.names) {
|
|
219
293
|
const normalized = normalize(name).trim();
|
|
220
|
-
if (!normalized) {
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (normalized.length < 3 && !UNAMBIGUOUS_SHORT_NAMES.has(normalized)) {
|
|
294
|
+
if (!normalized || !isIndexedName(normalized)) {
|
|
224
295
|
continue;
|
|
225
296
|
}
|
|
226
297
|
let slot = map.get(normalized);
|
|
@@ -279,6 +350,56 @@ function collectCandidates(normalizedText) {
|
|
|
279
350
|
}
|
|
280
351
|
return candidates;
|
|
281
352
|
}
|
|
353
|
+
function joinLines(text) {
|
|
354
|
+
const parts = [];
|
|
355
|
+
const offsets = [];
|
|
356
|
+
let cursor = 0;
|
|
357
|
+
for (const lineBreak of text.matchAll(/[^\S\n]*\n[^\S\n]*/g)) {
|
|
358
|
+
for (let i = cursor; i < lineBreak.index; i += 1) {
|
|
359
|
+
offsets.push(i);
|
|
360
|
+
}
|
|
361
|
+
parts.push(text.slice(cursor, lineBreak.index), " ");
|
|
362
|
+
offsets.push(lineBreak.index + lineBreak[0].indexOf("\n"));
|
|
363
|
+
cursor = lineBreak.index + lineBreak[0].length;
|
|
364
|
+
}
|
|
365
|
+
for (let i = cursor; i < text.length; i += 1) {
|
|
366
|
+
offsets.push(i);
|
|
367
|
+
}
|
|
368
|
+
parts.push(text.slice(cursor));
|
|
369
|
+
return { joined: parts.join(""), offsets };
|
|
370
|
+
}
|
|
371
|
+
function collectWrappedCandidates(normalizedText) {
|
|
372
|
+
if (!normalizedText.includes("\n")) {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
const { joined, offsets } = joinLines(normalizedText);
|
|
376
|
+
const namePatterns = getNamePatterns();
|
|
377
|
+
const candidates = [];
|
|
378
|
+
for (const [name, slot] of namePatterns) {
|
|
379
|
+
if (!name.includes(" ") || !joined.includes(name)) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const regex = slot.regex ??= buildNamePattern(name);
|
|
383
|
+
regex.lastIndex = 0;
|
|
384
|
+
let occurrences = 0;
|
|
385
|
+
let match = regex.exec(joined);
|
|
386
|
+
while (match !== null && occurrences < MAX_OCCURRENCES_PER_NAME) {
|
|
387
|
+
const start = offsets[match.index];
|
|
388
|
+
const end = offsets[match.index + match[0].length - 1] + 1;
|
|
389
|
+
const lineBreak = normalizedText.indexOf("\n", start);
|
|
390
|
+
const nextBreak = normalizedText.indexOf("\n", lineBreak + 1);
|
|
391
|
+
const wrapped = lineBreak !== -1 && lineBreak < end && (nextBreak === -1 || nextBreak >= end);
|
|
392
|
+
if (wrapped && !namePatterns.has(normalizedText.slice(start, lineBreak).trim()) && !hasValueEvidence(
|
|
393
|
+
normalizedText.slice(getLineBounds(normalizedText, start).start, lineBreak)
|
|
394
|
+
)) {
|
|
395
|
+
candidates.push({ end, entries: slot.entries, start });
|
|
396
|
+
occurrences += 1;
|
|
397
|
+
}
|
|
398
|
+
match = regex.exec(joined);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return candidates;
|
|
402
|
+
}
|
|
282
403
|
function resolveOverlaps(candidates) {
|
|
283
404
|
const sorted = [...candidates].sort(
|
|
284
405
|
(a, b) => b.end - b.start - (a.end - a.start) || a.start - b.start
|
|
@@ -299,33 +420,37 @@ function findBiomarkersInText(ocrText) {
|
|
|
299
420
|
const startTime = Date.now();
|
|
300
421
|
const normalizedText = normalize(ocrText);
|
|
301
422
|
const bestByCode = /* @__PURE__ */ new Map();
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
423
|
+
const contexts = /* @__PURE__ */ new Map();
|
|
424
|
+
const candidates = [
|
|
425
|
+
...collectCandidates(normalizedText),
|
|
426
|
+
...collectWrappedCandidates(normalizedText)
|
|
427
|
+
];
|
|
428
|
+
for (const candidate of resolveOverlaps(candidates)) {
|
|
429
|
+
const lineStart = getLineBounds(normalizedText, candidate.start).start;
|
|
430
|
+
const lineEnd = getLineBounds(normalizedText, candidate.end - 1).end;
|
|
431
|
+
const key = `${lineStart}:${lineEnd}`;
|
|
432
|
+
let context = contexts.get(key);
|
|
433
|
+
if (!context) {
|
|
434
|
+
const line = normalizedText.slice(lineStart, lineEnd);
|
|
435
|
+
context = {
|
|
436
|
+
genetic: hasGeneticContext(line),
|
|
437
|
+
girth: hasGirthContext(line),
|
|
438
|
+
hasValue: hasValueEvidence(line)
|
|
439
|
+
};
|
|
440
|
+
contexts.set(key, context);
|
|
311
441
|
}
|
|
442
|
+
const { genetic, girth, hasValue } = context;
|
|
312
443
|
if (genetic) {
|
|
313
444
|
continue;
|
|
314
445
|
}
|
|
315
|
-
|
|
316
|
-
if (hasValue === void 0) {
|
|
317
|
-
hasValue = hasValueEvidence(normalizedText.slice(lineStart, lineEnd));
|
|
318
|
-
valueLines.set(lineStart, hasValue);
|
|
319
|
-
}
|
|
320
|
-
let girth = girthLines.get(lineStart);
|
|
321
|
-
if (girth === void 0) {
|
|
322
|
-
girth = hasGirthContext(normalizedText.slice(lineStart, lineEnd));
|
|
323
|
-
girthLines.set(lineStart, girth);
|
|
324
|
-
}
|
|
446
|
+
const before = normalizedText.slice(lineStart, candidate.start);
|
|
325
447
|
for (const entry of candidate.entries) {
|
|
326
448
|
if (entry.ambiguous && !hasValue) {
|
|
327
449
|
continue;
|
|
328
450
|
}
|
|
451
|
+
if (qualifiedByBodyRegion(entry.code, before)) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
329
454
|
if (girth && SKINFOLD_SITE_CODES.has(entry.code)) {
|
|
330
455
|
continue;
|
|
331
456
|
}
|
|
@@ -628,7 +753,7 @@ function buildPrompt(text, allowed) {
|
|
|
628
753
|
}
|
|
629
754
|
function stripFence(raw) {
|
|
630
755
|
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(raw);
|
|
631
|
-
return (_nullishCoalesce(_optionalChain([fenced, 'optionalAccess',
|
|
756
|
+
return (_nullishCoalesce(_optionalChain([fenced, 'optionalAccess', _4 => _4[1]]), () => ( raw))).trim();
|
|
632
757
|
}
|
|
633
758
|
async function extractWithModel(text, options) {
|
|
634
759
|
const { apiKey, baseUrl, headers = {}, model, responseFormat, timeoutMs = 3e5 } = options;
|
|
@@ -668,7 +793,7 @@ async function extractWithModel(text, options) {
|
|
|
668
793
|
throw new Error(`${String(response.status)} de ${baseUrl}: ${await response.text()}`);
|
|
669
794
|
}
|
|
670
795
|
const body = await response.json();
|
|
671
|
-
const raw = _nullishCoalesce(_optionalChain([body, 'access',
|
|
796
|
+
const raw = _nullishCoalesce(_optionalChain([body, 'access', _5 => _5.choices, 'optionalAccess', _6 => _6[0], 'optionalAccess', _7 => _7.message, 'optionalAccess', _8 => _8.content]), () => ( ""));
|
|
672
797
|
const tookMs = Date.now() - startedAt;
|
|
673
798
|
try {
|
|
674
799
|
return { payload: JSON.parse(stripFence(raw)), raw, tookMs };
|