@precisa-saude/fhir-ocr-utils 0.34.0 → 0.35.1
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 +6 -0
- package/dist/cli.js +103 -27
- package/dist/index.cjs +103 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +102 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,6 +103,12 @@ por código, o de maior confiança.
|
|
|
103
103
|
`position` é o índice no texto normalizado (sem acentos, minúsculas, espaços
|
|
104
104
|
horizontais colapsados), não no texto OCR original.
|
|
105
105
|
|
|
106
|
+
Quando o biomarcador tem códigos LOINC por método e o texto afirma o método
|
|
107
|
+
perto do exame, o casamento traz `methodLoinc` e `methodCue`. A decisão é da
|
|
108
|
+
varredura; o `validateExtraction` troca o código por método que o modelo
|
|
109
|
+
devolver pelo que a varredura achou, ou pelo código sem método. Ver
|
|
110
|
+
[código LOINC por método](../../docs/biomarcadores.md#código-loinc-por-método).
|
|
111
|
+
|
|
106
112
|
#### Confiança
|
|
107
113
|
|
|
108
114
|
`match.confidence` reflete a qualidade do casamento — nome completo ao lado de
|
package/dist/cli.js
CHANGED
|
@@ -169,6 +169,87 @@ var STARTS_WITH_PERCENT = /^ ?(?:\( ?% ?\)|%|[-+]?\d+(?:[.,]\d+)? ?%)/;
|
|
|
169
169
|
function followedByPercent(code, after) {
|
|
170
170
|
return WHOLE_BODY_MASS_CODES.has(code) && STARTS_WITH_PERCENT.test(after);
|
|
171
171
|
}
|
|
172
|
+
var SKINFOLD_SITE_CODES = /* @__PURE__ */ new Set([
|
|
173
|
+
"SkinfoldAbdominal",
|
|
174
|
+
"SkinfoldChest",
|
|
175
|
+
"SkinfoldMidaxillary",
|
|
176
|
+
"SkinfoldSubscapular",
|
|
177
|
+
"SkinfoldSuprailiac",
|
|
178
|
+
"SkinfoldThigh",
|
|
179
|
+
"SkinfoldTriceps"
|
|
180
|
+
]);
|
|
181
|
+
var GIRTH_CONTEXT_PATTERNS = [
|
|
182
|
+
/\bcircumference\b/,
|
|
183
|
+
/\bcircunferencias?\b/,
|
|
184
|
+
/\bperimetros?\b/,
|
|
185
|
+
/\bgirth\b/
|
|
186
|
+
];
|
|
187
|
+
var SKINFOLD_CONTEXT_PATTERNS = [/\bdobras?\b/, /\bskin ?folds?\b/, /\bpregas?\b/];
|
|
188
|
+
var CENTIMETRE_VALUE = /\d\s*(?:,\d+\s*)?cm\b/;
|
|
189
|
+
function hasGirthContext(line) {
|
|
190
|
+
if (SKINFOLD_CONTEXT_PATTERNS.some((re) => re.test(line))) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
return GIRTH_CONTEXT_PATTERNS.some((re) => re.test(line)) || CENTIMETRE_VALUE.test(line);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/method-variant.ts
|
|
197
|
+
import { getDefinitionByCode } from "@precisa-saude/fhir";
|
|
198
|
+
var METHOD_CUE_LINES_BELOW = 6;
|
|
199
|
+
function findMethodVariant(code, position, lines, otherAnchorLines, normalizedText, matchesCue) {
|
|
200
|
+
const variants = getDefinitionByCode(code)?.methodVariants?.filter(
|
|
201
|
+
(v) => v.cues.en.length > 0 || v.cues.pt.length > 0
|
|
202
|
+
);
|
|
203
|
+
if (!variants || variants.length === 0) return void 0;
|
|
204
|
+
const index = lines.findIndex((l) => l.start <= position && position <= l.end);
|
|
205
|
+
if (index === -1) return void 0;
|
|
206
|
+
const window = [];
|
|
207
|
+
if (index > 0 && !otherAnchorLines.has(index - 1)) window.push(index - 1);
|
|
208
|
+
window.push(index);
|
|
209
|
+
for (let i = index + 1; i < lines.length && i <= index + METHOD_CUE_LINES_BELOW; i += 1) {
|
|
210
|
+
if (otherAnchorLines.has(i)) break;
|
|
211
|
+
window.push(i);
|
|
212
|
+
}
|
|
213
|
+
const text = window.map((i) => normalizedText.slice(lines[i].start, lines[i].end)).join("\n");
|
|
214
|
+
const found = variants.flatMap((variant) => {
|
|
215
|
+
const cue = [...variant.cues.pt, ...variant.cues.en].find((c) => matchesCue(text, c));
|
|
216
|
+
return cue ? [{ cue, loinc: variant.loinc }] : [];
|
|
217
|
+
});
|
|
218
|
+
return found.length === 1 ? found[0] : void 0;
|
|
219
|
+
}
|
|
220
|
+
function recordAnchorLine(anchoredLineStarts, lineStart, entries) {
|
|
221
|
+
const codes2 = anchoredLineStarts.get(lineStart) ?? /* @__PURE__ */ new Set();
|
|
222
|
+
for (const entry of entries) codes2.add(entry.code);
|
|
223
|
+
anchoredLineStarts.set(lineStart, codes2);
|
|
224
|
+
}
|
|
225
|
+
function attachMethodVariants(matches, normalizedText, anchoredLineStarts, matchesCue) {
|
|
226
|
+
const lines = [];
|
|
227
|
+
for (let start = 0; start <= normalizedText.length; ) {
|
|
228
|
+
const end = normalizedText.indexOf("\n", start);
|
|
229
|
+
lines.push({ end: end === -1 ? normalizedText.length : end, start });
|
|
230
|
+
if (end === -1) break;
|
|
231
|
+
start = end + 1;
|
|
232
|
+
}
|
|
233
|
+
for (const match of matches) {
|
|
234
|
+
const otherAnchorLines = /* @__PURE__ */ new Set();
|
|
235
|
+
lines.forEach((line, i) => {
|
|
236
|
+
const codes2 = anchoredLineStarts.get(line.start);
|
|
237
|
+
if (codes2 && [...codes2].some((c) => c !== match.code)) otherAnchorLines.add(i);
|
|
238
|
+
});
|
|
239
|
+
const variant = findMethodVariant(
|
|
240
|
+
match.code,
|
|
241
|
+
match.position,
|
|
242
|
+
lines,
|
|
243
|
+
otherAnchorLines,
|
|
244
|
+
normalizedText,
|
|
245
|
+
matchesCue
|
|
246
|
+
);
|
|
247
|
+
if (variant) {
|
|
248
|
+
match.methodLoinc = variant.loinc;
|
|
249
|
+
match.methodCue = variant.cue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
172
253
|
|
|
173
254
|
// src/anchor.ts
|
|
174
255
|
var CONFIDENCE_VALUE_ADJACENT = 1;
|
|
@@ -249,29 +330,6 @@ var GENETIC_CONTEXT_PATTERNS = [
|
|
|
249
330
|
/\bhomozigot/,
|
|
250
331
|
/\bsequence change\b/
|
|
251
332
|
];
|
|
252
|
-
var SKINFOLD_SITE_CODES = /* @__PURE__ */ new Set([
|
|
253
|
-
"SkinfoldAbdominal",
|
|
254
|
-
"SkinfoldChest",
|
|
255
|
-
"SkinfoldMidaxillary",
|
|
256
|
-
"SkinfoldSubscapular",
|
|
257
|
-
"SkinfoldSuprailiac",
|
|
258
|
-
"SkinfoldThigh",
|
|
259
|
-
"SkinfoldTriceps"
|
|
260
|
-
]);
|
|
261
|
-
var GIRTH_CONTEXT_PATTERNS = [
|
|
262
|
-
/\bcircumference\b/,
|
|
263
|
-
/\bcircunferencias?\b/,
|
|
264
|
-
/\bperimetros?\b/,
|
|
265
|
-
/\bgirth\b/
|
|
266
|
-
];
|
|
267
|
-
var SKINFOLD_CONTEXT_PATTERNS = [/\bdobras?\b/, /\bskin ?folds?\b/, /\bpregas?\b/];
|
|
268
|
-
var CENTIMETRE_VALUE = /\d\s*(?:,\d+\s*)?cm\b/;
|
|
269
|
-
function hasGirthContext(line) {
|
|
270
|
-
if (SKINFOLD_CONTEXT_PATTERNS.some((re) => re.test(line))) {
|
|
271
|
-
return false;
|
|
272
|
-
}
|
|
273
|
-
return GIRTH_CONTEXT_PATTERNS.some((re) => re.test(line)) || CENTIMETRE_VALUE.test(line);
|
|
274
|
-
}
|
|
275
333
|
var DIGIT_PATTERN = /\d/;
|
|
276
334
|
var cachedUnitTokens = null;
|
|
277
335
|
function getUnitTokens() {
|
|
@@ -444,6 +502,7 @@ function findBiomarkersInText(ocrText) {
|
|
|
444
502
|
const normalizedText = normalize(ocrText);
|
|
445
503
|
const bestByCode = /* @__PURE__ */ new Map();
|
|
446
504
|
const contexts = /* @__PURE__ */ new Map();
|
|
505
|
+
const anchoredLineStarts = /* @__PURE__ */ new Map();
|
|
447
506
|
const candidates = [
|
|
448
507
|
...collectCandidates(normalizedText),
|
|
449
508
|
...collectWrappedCandidates(normalizedText)
|
|
@@ -466,6 +525,7 @@ function findBiomarkersInText(ocrText) {
|
|
|
466
525
|
if (genetic) {
|
|
467
526
|
continue;
|
|
468
527
|
}
|
|
528
|
+
recordAnchorLine(anchoredLineStarts, lineStart, candidate.entries);
|
|
469
529
|
const before = normalizedText.slice(lineStart, candidate.start);
|
|
470
530
|
const after = normalizedText.slice(candidate.end, lineEnd);
|
|
471
531
|
for (const entry of candidate.entries) {
|
|
@@ -497,6 +557,12 @@ function findBiomarkersInText(ocrText) {
|
|
|
497
557
|
}
|
|
498
558
|
}
|
|
499
559
|
}
|
|
560
|
+
attachMethodVariants(
|
|
561
|
+
bestByCode.values(),
|
|
562
|
+
normalizedText,
|
|
563
|
+
anchoredLineStarts,
|
|
564
|
+
(text, cue) => buildNamePattern(normalize(cue).trim()).test(text)
|
|
565
|
+
);
|
|
500
566
|
const matches = Array.from(bestByCode.values()).sort((a, b) => a.position - b.position);
|
|
501
567
|
const scanTimeMs = Date.now() - startTime;
|
|
502
568
|
return {
|
|
@@ -514,7 +580,7 @@ function getMatchedCodes(result) {
|
|
|
514
580
|
}
|
|
515
581
|
|
|
516
582
|
// src/extraction-to-lab-result.ts
|
|
517
|
-
import { loincToCode } from "@precisa-saude/fhir";
|
|
583
|
+
import { loincToCode, methodVariantOf } from "@precisa-saude/fhir";
|
|
518
584
|
function flagFor(b) {
|
|
519
585
|
if (typeof b.value !== "number") return "";
|
|
520
586
|
if (typeof b.referenceMax === "number" && b.value > b.referenceMax) return "H";
|
|
@@ -533,6 +599,9 @@ function extractionToLabResult(biomarkers, options = {}) {
|
|
|
533
599
|
biomarkerCode: code,
|
|
534
600
|
biomarkerName: b.name,
|
|
535
601
|
flag: flagFor(b),
|
|
602
|
+
// O código por método já passou pela varredura no validador; aqui só
|
|
603
|
+
// atravessa, e só quando é variante declarada do biomarcador.
|
|
604
|
+
...b.loinc && methodVariantOf(code, b.loinc) ? { methodLoinc: b.loinc } : {},
|
|
536
605
|
...typeof b.referenceMax === "number" ? { referenceMax: b.referenceMax } : {},
|
|
537
606
|
...typeof b.referenceMin === "number" ? { referenceMin: b.referenceMin } : {},
|
|
538
607
|
reportId,
|
|
@@ -556,7 +625,7 @@ function extractionToLabResult(biomarkers, options = {}) {
|
|
|
556
625
|
}
|
|
557
626
|
|
|
558
627
|
// src/extraction-validator.ts
|
|
559
|
-
import { loincToCode as loincToCode2 } from "@precisa-saude/fhir";
|
|
628
|
+
import { codeToLoinc, getDefinitionByCode as getDefinitionByCode2, loincToCode as loincToCode2 } from "@precisa-saude/fhir";
|
|
560
629
|
|
|
561
630
|
// src/reference-bound.ts
|
|
562
631
|
var ATE = /(?:<|≤|<=|menor\s+que|menor\s+ou\s+igual|abaixo\s+de|at[ée]|under|less\s+than)\s*[:=]?\s*$/iu;
|
|
@@ -617,6 +686,13 @@ function allowedKeys(anchors) {
|
|
|
617
686
|
}
|
|
618
687
|
return allowed;
|
|
619
688
|
}
|
|
689
|
+
function withScannedMethod(biomarker, anchors) {
|
|
690
|
+
const code = biomarker.loinc ? loincToCode2(biomarker.loinc) : void 0;
|
|
691
|
+
if (!code || !getDefinitionByCode2(code)?.methodVariants?.length) return biomarker;
|
|
692
|
+
const scanned = anchors.matches.find((m) => m.code === code)?.methodLoinc;
|
|
693
|
+
const loinc = scanned ?? codeToLoinc(code);
|
|
694
|
+
return loinc && loinc !== biomarker.loinc ? { ...biomarker, loinc } : biomarker;
|
|
695
|
+
}
|
|
620
696
|
function validateExtraction(raw, options = {}) {
|
|
621
697
|
const { anchors } = options;
|
|
622
698
|
if (!isRecord(raw)) {
|
|
@@ -668,7 +744,7 @@ function validateExtraction(raw, options = {}) {
|
|
|
668
744
|
continue;
|
|
669
745
|
}
|
|
670
746
|
}
|
|
671
|
-
accepted.push(placeSingleBound(biomarker));
|
|
747
|
+
accepted.push(placeSingleBound(anchors ? withScannedMethod(biomarker, anchors) : biomarker));
|
|
672
748
|
}
|
|
673
749
|
return {
|
|
674
750
|
accepted,
|
|
@@ -1017,7 +1093,7 @@ async function main() {
|
|
|
1017
1093
|
strict: false
|
|
1018
1094
|
});
|
|
1019
1095
|
if (values.version) {
|
|
1020
|
-
process.stdout.write(`${"0.
|
|
1096
|
+
process.stdout.write(`${"0.35.1"}
|
|
1021
1097
|
`);
|
|
1022
1098
|
return;
|
|
1023
1099
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -153,6 +153,87 @@ var STARTS_WITH_PERCENT = /^ ?(?:\( ?% ?\)|%|[-+]?\d+(?:[.,]\d+)? ?%)/;
|
|
|
153
153
|
function followedByPercent(code, after) {
|
|
154
154
|
return WHOLE_BODY_MASS_CODES.has(code) && STARTS_WITH_PERCENT.test(after);
|
|
155
155
|
}
|
|
156
|
+
var SKINFOLD_SITE_CODES = /* @__PURE__ */ new Set([
|
|
157
|
+
"SkinfoldAbdominal",
|
|
158
|
+
"SkinfoldChest",
|
|
159
|
+
"SkinfoldMidaxillary",
|
|
160
|
+
"SkinfoldSubscapular",
|
|
161
|
+
"SkinfoldSuprailiac",
|
|
162
|
+
"SkinfoldThigh",
|
|
163
|
+
"SkinfoldTriceps"
|
|
164
|
+
]);
|
|
165
|
+
var GIRTH_CONTEXT_PATTERNS = [
|
|
166
|
+
/\bcircumference\b/,
|
|
167
|
+
/\bcircunferencias?\b/,
|
|
168
|
+
/\bperimetros?\b/,
|
|
169
|
+
/\bgirth\b/
|
|
170
|
+
];
|
|
171
|
+
var SKINFOLD_CONTEXT_PATTERNS = [/\bdobras?\b/, /\bskin ?folds?\b/, /\bpregas?\b/];
|
|
172
|
+
var CENTIMETRE_VALUE = /\d\s*(?:,\d+\s*)?cm\b/;
|
|
173
|
+
function hasGirthContext(line) {
|
|
174
|
+
if (SKINFOLD_CONTEXT_PATTERNS.some((re) => re.test(line))) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return GIRTH_CONTEXT_PATTERNS.some((re) => re.test(line)) || CENTIMETRE_VALUE.test(line);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/method-variant.ts
|
|
181
|
+
|
|
182
|
+
var METHOD_CUE_LINES_BELOW = 6;
|
|
183
|
+
function findMethodVariant(code, position, lines, otherAnchorLines, normalizedText, matchesCue) {
|
|
184
|
+
const variants = _optionalChain([_fhir.getDefinitionByCode.call(void 0, code), 'optionalAccess', _ => _.methodVariants, 'optionalAccess', _2 => _2.filter, 'call', _3 => _3(
|
|
185
|
+
(v) => v.cues.en.length > 0 || v.cues.pt.length > 0
|
|
186
|
+
)]);
|
|
187
|
+
if (!variants || variants.length === 0) return void 0;
|
|
188
|
+
const index = lines.findIndex((l) => l.start <= position && position <= l.end);
|
|
189
|
+
if (index === -1) return void 0;
|
|
190
|
+
const window = [];
|
|
191
|
+
if (index > 0 && !otherAnchorLines.has(index - 1)) window.push(index - 1);
|
|
192
|
+
window.push(index);
|
|
193
|
+
for (let i = index + 1; i < lines.length && i <= index + METHOD_CUE_LINES_BELOW; i += 1) {
|
|
194
|
+
if (otherAnchorLines.has(i)) break;
|
|
195
|
+
window.push(i);
|
|
196
|
+
}
|
|
197
|
+
const text = window.map((i) => normalizedText.slice(lines[i].start, lines[i].end)).join("\n");
|
|
198
|
+
const found = variants.flatMap((variant) => {
|
|
199
|
+
const cue = [...variant.cues.pt, ...variant.cues.en].find((c) => matchesCue(text, c));
|
|
200
|
+
return cue ? [{ cue, loinc: variant.loinc }] : [];
|
|
201
|
+
});
|
|
202
|
+
return found.length === 1 ? found[0] : void 0;
|
|
203
|
+
}
|
|
204
|
+
function recordAnchorLine(anchoredLineStarts, lineStart, entries) {
|
|
205
|
+
const codes = _nullishCoalesce(anchoredLineStarts.get(lineStart), () => ( /* @__PURE__ */ new Set()));
|
|
206
|
+
for (const entry of entries) codes.add(entry.code);
|
|
207
|
+
anchoredLineStarts.set(lineStart, codes);
|
|
208
|
+
}
|
|
209
|
+
function attachMethodVariants(matches, normalizedText, anchoredLineStarts, matchesCue) {
|
|
210
|
+
const lines = [];
|
|
211
|
+
for (let start = 0; start <= normalizedText.length; ) {
|
|
212
|
+
const end = normalizedText.indexOf("\n", start);
|
|
213
|
+
lines.push({ end: end === -1 ? normalizedText.length : end, start });
|
|
214
|
+
if (end === -1) break;
|
|
215
|
+
start = end + 1;
|
|
216
|
+
}
|
|
217
|
+
for (const match of matches) {
|
|
218
|
+
const otherAnchorLines = /* @__PURE__ */ new Set();
|
|
219
|
+
lines.forEach((line, i) => {
|
|
220
|
+
const codes = anchoredLineStarts.get(line.start);
|
|
221
|
+
if (codes && [...codes].some((c) => c !== match.code)) otherAnchorLines.add(i);
|
|
222
|
+
});
|
|
223
|
+
const variant = findMethodVariant(
|
|
224
|
+
match.code,
|
|
225
|
+
match.position,
|
|
226
|
+
lines,
|
|
227
|
+
otherAnchorLines,
|
|
228
|
+
normalizedText,
|
|
229
|
+
matchesCue
|
|
230
|
+
);
|
|
231
|
+
if (variant) {
|
|
232
|
+
match.methodLoinc = variant.loinc;
|
|
233
|
+
match.methodCue = variant.cue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
156
237
|
|
|
157
238
|
// src/anchor.ts
|
|
158
239
|
var CONFIDENCE_VALUE_ADJACENT = 1;
|
|
@@ -187,7 +268,7 @@ function getNamesByHead() {
|
|
|
187
268
|
return cachedNamesByHead;
|
|
188
269
|
}
|
|
189
270
|
function startsWithCatalogName(text) {
|
|
190
|
-
const head = _optionalChain([/^[\p{L}\p{N}]+/u, 'access',
|
|
271
|
+
const head = _optionalChain([/^[\p{L}\p{N}]+/u, 'access', _4 => _4.exec, 'call', _5 => _5(text), 'optionalAccess', _6 => _6[0]]);
|
|
191
272
|
if (!head) {
|
|
192
273
|
return false;
|
|
193
274
|
}
|
|
@@ -233,29 +314,6 @@ var GENETIC_CONTEXT_PATTERNS = [
|
|
|
233
314
|
/\bhomozigot/,
|
|
234
315
|
/\bsequence change\b/
|
|
235
316
|
];
|
|
236
|
-
var SKINFOLD_SITE_CODES = /* @__PURE__ */ new Set([
|
|
237
|
-
"SkinfoldAbdominal",
|
|
238
|
-
"SkinfoldChest",
|
|
239
|
-
"SkinfoldMidaxillary",
|
|
240
|
-
"SkinfoldSubscapular",
|
|
241
|
-
"SkinfoldSuprailiac",
|
|
242
|
-
"SkinfoldThigh",
|
|
243
|
-
"SkinfoldTriceps"
|
|
244
|
-
]);
|
|
245
|
-
var GIRTH_CONTEXT_PATTERNS = [
|
|
246
|
-
/\bcircumference\b/,
|
|
247
|
-
/\bcircunferencias?\b/,
|
|
248
|
-
/\bperimetros?\b/,
|
|
249
|
-
/\bgirth\b/
|
|
250
|
-
];
|
|
251
|
-
var SKINFOLD_CONTEXT_PATTERNS = [/\bdobras?\b/, /\bskin ?folds?\b/, /\bpregas?\b/];
|
|
252
|
-
var CENTIMETRE_VALUE = /\d\s*(?:,\d+\s*)?cm\b/;
|
|
253
|
-
function hasGirthContext(line) {
|
|
254
|
-
if (SKINFOLD_CONTEXT_PATTERNS.some((re) => re.test(line))) {
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
return GIRTH_CONTEXT_PATTERNS.some((re) => re.test(line)) || CENTIMETRE_VALUE.test(line);
|
|
258
|
-
}
|
|
259
317
|
var DIGIT_PATTERN = /\d/;
|
|
260
318
|
var cachedUnitTokens = null;
|
|
261
319
|
function getUnitTokens() {
|
|
@@ -428,6 +486,7 @@ function findBiomarkersInText(ocrText) {
|
|
|
428
486
|
const normalizedText = normalize(ocrText);
|
|
429
487
|
const bestByCode = /* @__PURE__ */ new Map();
|
|
430
488
|
const contexts = /* @__PURE__ */ new Map();
|
|
489
|
+
const anchoredLineStarts = /* @__PURE__ */ new Map();
|
|
431
490
|
const candidates = [
|
|
432
491
|
...collectCandidates(normalizedText),
|
|
433
492
|
...collectWrappedCandidates(normalizedText)
|
|
@@ -450,6 +509,7 @@ function findBiomarkersInText(ocrText) {
|
|
|
450
509
|
if (genetic) {
|
|
451
510
|
continue;
|
|
452
511
|
}
|
|
512
|
+
recordAnchorLine(anchoredLineStarts, lineStart, candidate.entries);
|
|
453
513
|
const before = normalizedText.slice(lineStart, candidate.start);
|
|
454
514
|
const after = normalizedText.slice(candidate.end, lineEnd);
|
|
455
515
|
for (const entry of candidate.entries) {
|
|
@@ -481,6 +541,12 @@ function findBiomarkersInText(ocrText) {
|
|
|
481
541
|
}
|
|
482
542
|
}
|
|
483
543
|
}
|
|
544
|
+
attachMethodVariants(
|
|
545
|
+
bestByCode.values(),
|
|
546
|
+
normalizedText,
|
|
547
|
+
anchoredLineStarts,
|
|
548
|
+
(text, cue) => buildNamePattern(normalize(cue).trim()).test(text)
|
|
549
|
+
);
|
|
484
550
|
const matches = Array.from(bestByCode.values()).sort((a, b) => a.position - b.position);
|
|
485
551
|
const scanTimeMs = Date.now() - startTime;
|
|
486
552
|
return {
|
|
@@ -591,6 +657,9 @@ function extractionToLabResult(biomarkers, options = {}) {
|
|
|
591
657
|
biomarkerCode: code,
|
|
592
658
|
biomarkerName: b.name,
|
|
593
659
|
flag: flagFor(b),
|
|
660
|
+
// O código por método já passou pela varredura no validador; aqui só
|
|
661
|
+
// atravessa, e só quando é variante declarada do biomarcador.
|
|
662
|
+
...b.loinc && _fhir.methodVariantOf.call(void 0, code, b.loinc) ? { methodLoinc: b.loinc } : {},
|
|
594
663
|
...typeof b.referenceMax === "number" ? { referenceMax: b.referenceMax } : {},
|
|
595
664
|
...typeof b.referenceMin === "number" ? { referenceMin: b.referenceMin } : {},
|
|
596
665
|
reportId,
|
|
@@ -675,6 +744,13 @@ function allowedKeys(anchors) {
|
|
|
675
744
|
}
|
|
676
745
|
return allowed;
|
|
677
746
|
}
|
|
747
|
+
function withScannedMethod(biomarker, anchors) {
|
|
748
|
+
const code = biomarker.loinc ? _fhir.loincToCode.call(void 0, biomarker.loinc) : void 0;
|
|
749
|
+
if (!code || !_optionalChain([_fhir.getDefinitionByCode.call(void 0, code), 'optionalAccess', _7 => _7.methodVariants, 'optionalAccess', _8 => _8.length])) return biomarker;
|
|
750
|
+
const scanned = _optionalChain([anchors, 'access', _9 => _9.matches, 'access', _10 => _10.find, 'call', _11 => _11((m) => m.code === code), 'optionalAccess', _12 => _12.methodLoinc]);
|
|
751
|
+
const loinc = _nullishCoalesce(scanned, () => ( _fhir.codeToLoinc.call(void 0, code)));
|
|
752
|
+
return loinc && loinc !== biomarker.loinc ? { ...biomarker, loinc } : biomarker;
|
|
753
|
+
}
|
|
678
754
|
function validateExtraction(raw, options = {}) {
|
|
679
755
|
const { anchors } = options;
|
|
680
756
|
if (!isRecord(raw)) {
|
|
@@ -726,7 +802,7 @@ function validateExtraction(raw, options = {}) {
|
|
|
726
802
|
continue;
|
|
727
803
|
}
|
|
728
804
|
}
|
|
729
|
-
accepted.push(placeSingleBound(biomarker));
|
|
805
|
+
accepted.push(placeSingleBound(anchors ? withScannedMethod(biomarker, anchors) : biomarker));
|
|
730
806
|
}
|
|
731
807
|
return {
|
|
732
808
|
accepted,
|
|
@@ -761,7 +837,7 @@ function buildPrompt(text, allowed) {
|
|
|
761
837
|
}
|
|
762
838
|
function stripFence(raw) {
|
|
763
839
|
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(raw);
|
|
764
|
-
return (_nullishCoalesce(_optionalChain([fenced, 'optionalAccess',
|
|
840
|
+
return (_nullishCoalesce(_optionalChain([fenced, 'optionalAccess', _13 => _13[1]]), () => ( raw))).trim();
|
|
765
841
|
}
|
|
766
842
|
async function extractWithModel(text, options) {
|
|
767
843
|
const { apiKey, baseUrl, headers = {}, model, responseFormat, timeoutMs = 3e5 } = options;
|
|
@@ -801,7 +877,7 @@ async function extractWithModel(text, options) {
|
|
|
801
877
|
throw new Error(`${String(response.status)} de ${baseUrl}: ${await response.text()}`);
|
|
802
878
|
}
|
|
803
879
|
const body = await response.json();
|
|
804
|
-
const raw = _nullishCoalesce(_optionalChain([body, 'access',
|
|
880
|
+
const raw = _nullishCoalesce(_optionalChain([body, 'access', _14 => _14.choices, 'optionalAccess', _15 => _15[0], 'optionalAccess', _16 => _16.message, 'optionalAccess', _17 => _17.content]), () => ( ""));
|
|
805
881
|
const tookMs = Date.now() - startedAt;
|
|
806
882
|
try {
|
|
807
883
|
return { payload: JSON.parse(stripFence(raw)), raw, tookMs };
|