med-pdf-nmo 0.1.0

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +298 -0
  3. package/README.ru.md +298 -0
  4. package/dist/bm25.d.ts +47 -0
  5. package/dist/bm25.js +86 -0
  6. package/dist/browser-shims/buffer.d.ts +30 -0
  7. package/dist/browser-shims/buffer.js +31 -0
  8. package/dist/browser-shims/crypto.d.ts +33 -0
  9. package/dist/browser-shims/crypto.js +45 -0
  10. package/dist/browser-shims/fs-promises.d.ts +13 -0
  11. package/dist/browser-shims/fs-promises.js +25 -0
  12. package/dist/browser-shims/fs.d.ts +14 -0
  13. package/dist/browser-shims/fs.js +24 -0
  14. package/dist/browser-shims/globals.d.ts +9 -0
  15. package/dist/browser-shims/globals.js +23 -0
  16. package/dist/browser-shims/path.d.ts +57 -0
  17. package/dist/browser-shims/path.js +65 -0
  18. package/dist/browser-shims/process.d.ts +22 -0
  19. package/dist/browser-shims/process.js +27 -0
  20. package/dist/browser.d.ts +9 -0
  21. package/dist/browser.js +12 -0
  22. package/dist/chunk.d.ts +15 -0
  23. package/dist/chunk.js +76 -0
  24. package/dist/cli.d.ts +2 -0
  25. package/dist/cli.js +87 -0
  26. package/dist/index.d.ts +82 -0
  27. package/dist/index.js +51 -0
  28. package/dist/med-pdf-nmo.browser.js +40413 -0
  29. package/dist/med-pdf-nmo.browser.mjs +40395 -0
  30. package/dist/normalize.d.ts +73 -0
  31. package/dist/normalize.js +477 -0
  32. package/dist/pdf.d.ts +35 -0
  33. package/dist/pdf.js +396 -0
  34. package/dist/predictor/config.d.ts +28 -0
  35. package/dist/predictor/config.js +26 -0
  36. package/dist/predictor/constants.d.ts +3 -0
  37. package/dist/predictor/constants.js +59 -0
  38. package/dist/predictor/runtime.d.ts +15 -0
  39. package/dist/predictor/runtime.js +59 -0
  40. package/dist/predictor/scorers/biomedical-symbols.d.ts +36 -0
  41. package/dist/predictor/scorers/biomedical-symbols.js +347 -0
  42. package/dist/predictor/scorers/coordinate-table.d.ts +82 -0
  43. package/dist/predictor/scorers/coordinate-table.js +1210 -0
  44. package/dist/predictor/scorers/direction.d.ts +71 -0
  45. package/dist/predictor/scorers/direction.js +345 -0
  46. package/dist/predictor/scorers/drug-dose.d.ts +6 -0
  47. package/dist/predictor/scorers/drug-dose.js +221 -0
  48. package/dist/predictor/scorers/exact-answer.d.ts +10 -0
  49. package/dist/predictor/scorers/exact-answer.js +75 -0
  50. package/dist/predictor/scorers/fibrosis-stage.d.ts +6 -0
  51. package/dist/predictor/scorers/fibrosis-stage.js +103 -0
  52. package/dist/predictor/scorers/focused.d.ts +40 -0
  53. package/dist/predictor/scorers/focused.js +204 -0
  54. package/dist/predictor/scorers/frequency.d.ts +10 -0
  55. package/dist/predictor/scorers/frequency.js +203 -0
  56. package/dist/predictor/scorers/numeric.d.ts +77 -0
  57. package/dist/predictor/scorers/numeric.js +1161 -0
  58. package/dist/predictor/scorers/recommendation-item.d.ts +27 -0
  59. package/dist/predictor/scorers/recommendation-item.js +469 -0
  60. package/dist/predictor/scorers/search.d.ts +41 -0
  61. package/dist/predictor/scorers/search.js +515 -0
  62. package/dist/predictor/selection.d.ts +30 -0
  63. package/dist/predictor/selection.js +370 -0
  64. package/dist/predictor/text-utils.d.ts +49 -0
  65. package/dist/predictor/text-utils.js +497 -0
  66. package/dist/predictor/types.d.ts +23 -0
  67. package/dist/predictor/types.js +1 -0
  68. package/dist/predictor.d.ts +52 -0
  69. package/dist/predictor.js +3834 -0
  70. package/package.json +82 -0
@@ -0,0 +1,497 @@
1
+ import { extractNumbers, normalizeForSearch, normalizeText, phraseTokens, stemToken, tokenize } from "../normalize.js";
2
+ /**
3
+ * Возвращает имя ближайшей к центру локального окна группы cue.
4
+ *
5
+ * Общий хелпер: используется temporal/condition scorer'ами, чтобы выбрать,
6
+ * какой из взаимоисключающих наборов подсказок (например, день/ночь или
7
+ * статусы) ближе к фокусу.
8
+ */
9
+ export function nearestCueName(local, entries) {
10
+ const center = Math.floor(local.length / 2);
11
+ let best = null;
12
+ for (const [name, cues] of entries) {
13
+ for (const cueText of cues) {
14
+ const cue = normalizeForSearch(cueText);
15
+ for (let index = local.indexOf(cue); index >= 0; index = local.indexOf(cue, index + cue.length)) {
16
+ const distance = Math.abs(index - center);
17
+ if (!best || distance < best.distance)
18
+ best = { name, distance };
19
+ }
20
+ }
21
+ }
22
+ return best?.name ?? null;
23
+ }
24
+ export function rawTokens(text) {
25
+ return normalizeText(text).match(/[a-zа-я0-9]+/giu) ?? [];
26
+ }
27
+ /**
28
+ * Проверяет вхождение токена с границами по пробелам/краям строки.
29
+ *
30
+ * Общий хелпер: не дает короткому токену совпасть как подстроке внутри
31
+ * другого слова (например, `i` внутри `ii`).
32
+ */
33
+ export function tokenBoundaryIncludes(normalizedText, normalizedToken) {
34
+ if (!normalizedText || !normalizedToken)
35
+ return false;
36
+ const pattern = new RegExp(`(^|\\s)${escapeRegExp(normalizedToken)}(\\s|$)`, "iu");
37
+ return pattern.test(normalizedText);
38
+ }
39
+ export function findPhraseOccurrences(text, phrase, { textIsNormalized = false } = {}) {
40
+ const normalizedText = textIsNormalized ? String(text ?? "") : normalizeForSearch(text);
41
+ const normalizedPhrase = normalizeForSearch(phrase);
42
+ if (!normalizedText || !normalizedPhrase || normalizedPhrase.length < 2)
43
+ return [];
44
+ const hits = [];
45
+ let start = 0;
46
+ while (start < normalizedText.length) {
47
+ const index = normalizedText.indexOf(normalizedPhrase, start);
48
+ if (index < 0)
49
+ break;
50
+ hits.push(index);
51
+ start = index + Math.max(1, normalizedPhrase.length);
52
+ if (hits.length > 80)
53
+ break;
54
+ }
55
+ return hits;
56
+ }
57
+ export function hasSearchBoundaries(text, index, length) {
58
+ const before = index > 0 ? text[index - 1] : "";
59
+ const after = index + length < text.length ? text[index + length] : "";
60
+ return !isSearchTokenChar(before) && !isSearchTokenChar(after);
61
+ }
62
+ function isSearchTokenChar(char) {
63
+ return !!char && /[a-zа-я0-9%./+-]/iu.test(char);
64
+ }
65
+ const THERAPY_ALIAS_GROUPS = [
66
+ [
67
+ "мгт",
68
+ "гормонотерапия",
69
+ "гормонотерапии",
70
+ "гормонотерапию",
71
+ "гормональная терапия",
72
+ "гормональной терапии",
73
+ "гормональную терапию",
74
+ "менопаузальная гормональная терапия",
75
+ "менопаузальной гормональной терапии",
76
+ "терапия гормонами",
77
+ "терапии гормонами",
78
+ ],
79
+ [
80
+ "антибиотикотерапия",
81
+ "антибиотикотерапии",
82
+ "антибактериальная терапия",
83
+ "антибактериальной терапии",
84
+ "терапия антибиотиками",
85
+ "терапии антибиотиками",
86
+ ],
87
+ [
88
+ "кислородотерапия",
89
+ "кислородотерапии",
90
+ "оксигенотерапия",
91
+ "оксигенотерапии",
92
+ "кислородная терапия",
93
+ "кислородной терапии",
94
+ "терапия кислородом",
95
+ "терапии кислородом",
96
+ ],
97
+ ["инсулинотерапия", "инсулинотерапии", "инсулиновая терапия", "инсулиновой терапии", "терапия инсулином", "терапии инсулином"],
98
+ ["иммунотерапия", "иммунотерапии", "иммунная терапия", "иммунной терапии"],
99
+ ["фармакотерапия", "фармакотерапии", "лекарственная терапия", "лекарственной терапии", "медикаментозная терапия", "медикаментозной терапии"],
100
+ ["физиотерапия", "физиотерапии", "физиотерапевтическое лечение", "физиотерапевтического лечения"],
101
+ ["психотерапия", "психотерапии", "психотерапию", "психотерапевтическое лечение", "психотерапевтического лечения"],
102
+ ["радиотерапия", "радиотерапии", "лучевая терапия", "лучевой терапии"],
103
+ [
104
+ "глюкокортикостероидная терапия",
105
+ "глюкокортикостероидной терапии",
106
+ "кортикостероидная терапия",
107
+ "кортикостероидной терапии",
108
+ "терапия глюкокортикостероидами",
109
+ "терапии глюкокортикостероидами",
110
+ "терапия гкс",
111
+ "терапия сгкс",
112
+ ],
113
+ ["противовирусная терапия", "противовирусной терапии", "противовирусное лечение", "пвт"],
114
+ ["синдром поликистозных яичников", "спя"],
115
+ ["рак эндометрия", "рака эндометрия", "рэ"],
116
+ ["антидотная терапия", "антидотной терапии", "терапия антидотами", "терапии антидотами"],
117
+ [
118
+ "дезинтоксикационная терапия",
119
+ "дезинтоксикационной терапии",
120
+ "детоксикационная терапия",
121
+ "детоксикационной терапии",
122
+ "дезинтоксикационное лечение",
123
+ "детоксикационное лечение",
124
+ ],
125
+ ];
126
+ /**
127
+ * Добавляет безопасные медицинские синонимы для поиска ответа в PDF.
128
+ *
129
+ * Словарь намеренно маленький: в него входят только общеупотребимые формы
130
+ * `X-терапия` / `X терапия` / `терапия X`, а также несколько устойчивых
131
+ * русских медицинских сокращений (`СПЯ`, `РЭ`). Более контекстные группы не
132
+ * добавляются, чтобы не усиливать варианты, которые встречаются рядом с
133
+ * противопоказаниями или условиями назначения.
134
+ */
135
+ function addMedicalAliasPhrases(phrases, answerText) {
136
+ const normalizedAnswer = normalizeForSearch(answerText);
137
+ for (const group of THERAPY_ALIAS_GROUPS) {
138
+ if (!group.some((term) => normalizedAnswer.includes(normalizeForSearch(term))))
139
+ continue;
140
+ for (const term of group)
141
+ phrases.add(term);
142
+ }
143
+ }
144
+ /**
145
+ * Добавляет устойчивые варианты единиц времени, которые в клинических текстах
146
+ * часто пишутся то полностью, то сокращенно: `6 часов` / `6 ч`.
147
+ */
148
+ function addTimeUnitAliasPhrases(phrases, answerText) {
149
+ const raw = normalizeText(answerText);
150
+ const numbers = extractNumbers(answerText);
151
+ if (!numbers.length)
152
+ return;
153
+ if (/(?:^|\s)(?:\u0447|\u0447\.|\u0447\u0430\u0441|\u0447\u0430\u0441\u0430|\u0447\u0430\u0441\u043e\u0432)(?:\s|$)/u.test(raw)) {
154
+ for (const number of numbers) {
155
+ phrases.add(`${number} \u0447`);
156
+ phrases.add(`${number} \u0447.`);
157
+ phrases.add(`${number} \u0447\u0430\u0441`);
158
+ phrases.add(`${number} \u0447\u0430\u0441\u0430`);
159
+ phrases.add(`${number} \u0447\u0430\u0441\u043e\u0432`);
160
+ }
161
+ }
162
+ }
163
+ export function answerSearchPhrases(answerText) {
164
+ const normalized = normalizeForSearch(answerText);
165
+ const phrases = new Set([answerText, normalized]);
166
+ addMedicalAliasPhrases(phrases, answerText);
167
+ addTimeUnitAliasPhrases(phrases, answerText);
168
+ const withoutParentheses = normalized.replace(/\([^)]*\)/g, " ").replace(/\s+/g, " ").trim();
169
+ if (withoutParentheses)
170
+ phrases.add(withoutParentheses);
171
+ const rawAnswerText = String(answerText ?? "");
172
+ const rawHyphenSplit = rawAnswerText.replace(/\s*[-\u2010-\u2015]\s*/g, " ").replace(/\s+/g, " ").trim();
173
+ if (rawHyphenSplit)
174
+ phrases.add(rawHyphenSplit);
175
+ const hyphenSplit = normalizeForSearch(rawAnswerText.replace(/\s*[-\u2010-\u2015]\s*/g, " "));
176
+ if (hyphenSplit)
177
+ phrases.add(hyphenSplit);
178
+ const rawHyphenSpaced = rawAnswerText.replace(/\s*[-\u2010-\u2015]\s*/g, " - ").replace(/\s+/g, " ").trim();
179
+ if (rawHyphenSpaced)
180
+ phrases.add(rawHyphenSpaced);
181
+ const hyphenSpaced = normalizeForSearch(rawAnswerText.replace(/\s*[-\u2010-\u2015]\s*/g, " - "));
182
+ if (hyphenSpaced)
183
+ phrases.add(hyphenSpaced);
184
+ const inhibitorMatch = rawAnswerText.match(new RegExp(`\u0438\u043d\u0433\u0438\u0431\u0438\u0442\\S*\\s+([A-Z\\u0410-\\u042F]{2,8})(.*)$`, "iu"));
185
+ if (inhibitorMatch?.[1]) {
186
+ const abbreviated = `\u0438${inhibitorMatch[1]}${inhibitorMatch[2] ?? ""}`.replace(/\s+/g, " ").trim();
187
+ if (abbreviated) {
188
+ phrases.add(abbreviated);
189
+ phrases.add(abbreviated.replace(/\s*\/\s*/g, " / ").replace(/\s+/g, " ").trim());
190
+ phrases.add(abbreviated.replace(/\s*\/\s*/g, " ").replace(/\s+/g, " ").trim());
191
+ phrases.add(normalizeForSearch(abbreviated));
192
+ }
193
+ }
194
+ if (withoutParentheses.includes("/")) {
195
+ phrases.add(withoutParentheses.replace(/\s*\/\s*/g, " ").replace(/\s+/g, " ").trim());
196
+ phrases.add(withoutParentheses.replace(/\s*\/\s*/g, " и ").replace(/\s+/g, " ").trim());
197
+ }
198
+ const withoutUnits = withoutParentheses
199
+ .replace(/\b(ме|мг|мкг|г|мл|л|мм|см|сут|час|день|дня|дней|неделя|недели|недель)\b(?:\s*\/\s*\b(мл|л|сут|час)\b)?/g, " ")
200
+ .replace(/\b(рт\.?\s*ст\.?|log\s*10\s*ме\s*\/\s*мл|ме\s*\/\s*мл|мг\s*\/\s*л|ммоль\s*\/\s*л)\b/g, " ")
201
+ .replace(/\s+/g, " ")
202
+ .trim();
203
+ if (withoutUnits)
204
+ phrases.add(withoutUnits);
205
+ const tokens = phraseTokens(withoutUnits || normalized);
206
+ if (tokens.length >= 3) {
207
+ phrases.add(tokens.slice(0, Math.min(tokens.length, 5)).join(" "));
208
+ }
209
+ const numbers = extractNumbers(answerText);
210
+ if (numbers.length === 1) {
211
+ const aroundNumber = normalized.match(new RegExp(`(?:\\S+\\s+){0,2}${numbers[0].replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s+\\S+){0,3}`));
212
+ if (aroundNumber?.[0])
213
+ phrases.add(aroundNumber[0]);
214
+ }
215
+ return [...phrases].filter((phrase) => normalizeForSearch(phrase).length >= 2);
216
+ }
217
+ export function focusedAnswerSearchPhrases(answerText) {
218
+ const phrases = new Set();
219
+ const plusParts = normalizeText(answerText)
220
+ .split(/\s*\+\s*/u)
221
+ .map((part) => part.trim())
222
+ .filter(Boolean);
223
+ if (plusParts.length > 1) {
224
+ phrases.add(plusParts.join(" + "));
225
+ const firstTokens = rawTokens(plusParts[0]);
226
+ if (firstTokens.length > 1) {
227
+ phrases.add([firstTokens.slice(1).join(" "), ...plusParts.slice(1)].join(" + "));
228
+ }
229
+ }
230
+ const tokens = rawTokens(answerText);
231
+ if (tokens.length >= 3) {
232
+ for (const length of [6, 5, 4]) {
233
+ if (tokens.length < length)
234
+ continue;
235
+ for (let index = 0; index <= tokens.length - length; index += 1) {
236
+ phrases.add(tokens.slice(index, index + length).join(" "));
237
+ }
238
+ }
239
+ }
240
+ for (const phrase of answerSearchPhrases(answerText))
241
+ phrases.add(phrase);
242
+ return [...phrases].filter((phrase) => normalizeForSearch(phrase).length >= 2);
243
+ }
244
+ export function containsPhrase(haystack, needle) {
245
+ const normalizedNeedle = normalizeForSearch(needle);
246
+ if (!normalizedNeedle)
247
+ return false;
248
+ return normalizeForSearch(haystack).includes(normalizedNeedle);
249
+ }
250
+ export function containsNormalizedPhrase(normalizedHaystack, needle) {
251
+ const normalizedNeedle = normalizeForSearch(needle);
252
+ if (!normalizedNeedle)
253
+ return false;
254
+ return String(normalizedHaystack ?? "").includes(normalizedNeedle);
255
+ }
256
+ export function tokenizeNormalized(text) {
257
+ return (String(text ?? "").match(/[a-zа-я0-9]+(?:[.%/+-][a-zа-я0-9]+)*/giu) ?? []).map((token) => stemToken(token));
258
+ }
259
+ export function tokenSequenceIncludes(haystackTokens, needleTokens) {
260
+ if (!needleTokens.length || needleTokens.length > haystackTokens.length)
261
+ return false;
262
+ for (let index = 0; index <= haystackTokens.length - needleTokens.length; index += 1) {
263
+ let ok = true;
264
+ for (let offset = 0; offset < needleTokens.length; offset += 1) {
265
+ if (haystackTokens[index + offset] !== needleTokens[offset]) {
266
+ ok = false;
267
+ break;
268
+ }
269
+ }
270
+ if (ok)
271
+ return true;
272
+ }
273
+ return false;
274
+ }
275
+ export function rawSoftCoverage(queryTokens, documentTokens) {
276
+ if (!queryTokens.length || !documentTokens.length)
277
+ return 0;
278
+ let hit = 0;
279
+ for (const token of queryTokens) {
280
+ if (/^(?:[ivx]+|\d+)$/iu.test(token)) {
281
+ if (documentTokens.includes(token))
282
+ hit += 1;
283
+ continue;
284
+ }
285
+ const prefixLength = Math.min(10, Math.max(4, token.length - 2));
286
+ const prefix = token.slice(0, prefixLength);
287
+ if (documentTokens.some((candidate) => candidate === token || candidate.startsWith(prefix) || token.startsWith(candidate.slice(0, prefixLength)))) {
288
+ hit += 1;
289
+ }
290
+ }
291
+ return hit / queryTokens.length;
292
+ }
293
+ export function escapeRegExp(value) {
294
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
295
+ }
296
+ export function softCoverage(queryTokens, documentTokens) {
297
+ if (!queryTokens.length || !documentTokens.length)
298
+ return 0;
299
+ const doc = [...new Set(documentTokens)];
300
+ let hit = 0;
301
+ for (const token of new Set(queryTokens)) {
302
+ const prefixLength = Math.min(8, Math.max(4, token.length - 2));
303
+ const prefix = token.slice(0, prefixLength);
304
+ if (doc.some((candidate) => candidate === token || candidate.startsWith(prefix) || token.startsWith(candidate.slice(0, prefixLength)))) {
305
+ hit += 1;
306
+ }
307
+ }
308
+ return hit / new Set(queryTokens).size;
309
+ }
310
+ export function strictSoftCoverage(queryTokens, documentTokens) {
311
+ if (!queryTokens.length || !documentTokens.length)
312
+ return 0;
313
+ const doc = [...new Set(documentTokens)];
314
+ let hit = 0;
315
+ for (const token of new Set(queryTokens)) {
316
+ if (doc.includes(token)) {
317
+ hit += 1;
318
+ continue;
319
+ }
320
+ if (token.length < 8)
321
+ continue;
322
+ const prefixLength = Math.min(10, Math.max(7, token.length - 3));
323
+ const prefix = token.slice(0, prefixLength);
324
+ if (doc.some((candidate) => candidate.length >= 8 && (candidate.startsWith(prefix) || token.startsWith(candidate.slice(0, prefixLength))))) {
325
+ hit += 1;
326
+ }
327
+ }
328
+ return hit / new Set(queryTokens).size;
329
+ }
330
+ export function tokenHitCount(queryTokens, documentTokens) {
331
+ if (!queryTokens.length || !documentTokens.length)
332
+ return 0;
333
+ const doc = new Set(documentTokens);
334
+ let hit = 0;
335
+ for (const token of new Set(queryTokens)) {
336
+ if (doc.has(token))
337
+ hit += 1;
338
+ }
339
+ return hit;
340
+ }
341
+ export function evidenceFromChunk(answerIdValue, chunk, score, kind) {
342
+ return {
343
+ answerId: answerIdValue,
344
+ page: chunk.page,
345
+ text: chunk.text.slice(0, 900),
346
+ score,
347
+ kind,
348
+ };
349
+ }
350
+ export function betterEvidence(left, right) {
351
+ if (!right)
352
+ return left;
353
+ if (!left || right.score > left.score)
354
+ return right;
355
+ return left;
356
+ }
357
+ export function evidenceSnippet(pageText, ...needles) {
358
+ const clean = String(pageText ?? "").replace(/\s+/g, " ").trim();
359
+ if (!clean)
360
+ return "";
361
+ const normalizedPage = normalizeForSearch(clean);
362
+ let bestIndex = -1;
363
+ for (const needle of needles) {
364
+ const normalizedNeedle = normalizeForSearch(needle);
365
+ if (!normalizedNeedle)
366
+ continue;
367
+ const index = normalizedPage.indexOf(normalizedNeedle.slice(0, Math.min(80, normalizedNeedle.length)));
368
+ if (index >= 0 && (bestIndex < 0 || index < bestIndex))
369
+ bestIndex = index;
370
+ }
371
+ if (bestIndex < 0)
372
+ return clean.slice(0, 900);
373
+ const start = Math.max(0, bestIndex - 300);
374
+ const end = Math.min(clean.length, bestIndex + 700);
375
+ return clean.slice(start, end);
376
+ }
377
+ export function numberCoverage(answer, text) {
378
+ const answerNumbers = extractNumbers(answer).flatMap(expandNumberToken);
379
+ if (!answerNumbers.length)
380
+ return 0;
381
+ const textNumbers = new Set(extractNumbersWithOcrJoins(text).flatMap(expandNumberToken));
382
+ if (!textNumbers.size)
383
+ return 0;
384
+ let hit = 0;
385
+ for (const number of answerNumbers) {
386
+ if (textNumbers.has(number))
387
+ hit += 1;
388
+ }
389
+ return hit / answerNumbers.length;
390
+ }
391
+ /**
392
+ * Возвращает числа из текста и добавляет типичный OCR-вариант, когда одно число
393
+ * разорвано пробелом (`9 00 мг` вместо `900 мг`). Склейка ограничена короткими
394
+ * группами цифр, чтобы не превращать обычные перечисления в произвольные числа.
395
+ */
396
+ function extractNumbersWithOcrJoins(text) {
397
+ const normalized = normalizeForSearch(text);
398
+ const numbers = extractNumbers(normalized);
399
+ const joined = normalized.match(/\b\d{1,2}\s+\d{2,3}\b/gu) ?? [];
400
+ for (const item of joined) {
401
+ numbers.push(item.replace(/\s+/gu, ""));
402
+ }
403
+ return numbers;
404
+ }
405
+ export function expandNumberToken(token) {
406
+ const cleaned = String(token).replace("%", "");
407
+ const parts = cleaned.split("-").filter(Boolean);
408
+ const out = [];
409
+ for (const part of parts) {
410
+ if (/^0+\d/.test(part))
411
+ out.push(part.replace(/^0+/, "") || "0");
412
+ const value = Number(part);
413
+ if (!Number.isFinite(value)) {
414
+ out.push(part);
415
+ continue;
416
+ }
417
+ out.push(String(value));
418
+ if (Number.isInteger(value) && value > 1)
419
+ out.push(String(value - 1));
420
+ }
421
+ return out;
422
+ }
423
+ export function tokenProximity(questionTokens, answerTokens, documentTokens) {
424
+ if (!questionTokens.length || !answerTokens.length || !documentTokens.length)
425
+ return 0;
426
+ const qSet = new Set(questionTokens);
427
+ const aSet = new Set(answerTokens);
428
+ const qPositions = [];
429
+ const aPositions = [];
430
+ documentTokens.forEach((token, index) => {
431
+ if (qSet.has(token))
432
+ qPositions.push(index);
433
+ if (aSet.has(token))
434
+ aPositions.push(index);
435
+ });
436
+ if (!qPositions.length || !aPositions.length)
437
+ return 0;
438
+ let total = 0;
439
+ for (const aPos of aPositions) {
440
+ let best = Infinity;
441
+ for (const qPos of qPositions) {
442
+ best = Math.min(best, Math.abs(aPos - qPos));
443
+ }
444
+ total += Math.exp(-best / 18);
445
+ }
446
+ return total / aPositions.length;
447
+ }
448
+ export function cachedPageTokens(page) {
449
+ if (!page.__tokens)
450
+ Object.defineProperty(page, "__tokens", { value: tokenize(page.text), enumerable: false });
451
+ return page.__tokens;
452
+ }
453
+ export function lineWindowSegments(page, radius = 2) {
454
+ const lines = page.lines ?? [];
455
+ const segments = [];
456
+ for (let index = 0; index < lines.length; index += 1) {
457
+ const text = lines.slice(index, Math.min(lines.length, index + radius + 1)).join(" ").replace(/\s+/g, " ").trim();
458
+ if (text.length >= 16 && text.length <= 900) {
459
+ segments.push({
460
+ text,
461
+ normalized: normalizeForSearch(text),
462
+ tokens: tokenize(text),
463
+ });
464
+ }
465
+ }
466
+ return segments;
467
+ }
468
+ export function cachedLineWindowSegments(page) {
469
+ if (!page.__lineWindowSegments) {
470
+ Object.defineProperty(page, "__lineWindowSegments", {
471
+ value: lineWindowSegments(page, 3),
472
+ enumerable: false,
473
+ });
474
+ }
475
+ return page.__lineWindowSegments;
476
+ }
477
+ export function pageWindow(page, center, radius = 1000) {
478
+ const normalized = page.normalized;
479
+ const start = Math.max(0, center - radius);
480
+ const end = Math.min(normalized.length, center + radius);
481
+ return normalized.slice(start, end);
482
+ }
483
+ export function proximityBonus(distance, radius) {
484
+ if (distance < 0 || distance > radius)
485
+ return 0;
486
+ return 1 - distance / radius;
487
+ }
488
+ export function answerHasQuestionNumbers(answer, question) {
489
+ const answerNumbers = new Set(extractNumbers(answer));
490
+ if (!answerNumbers.size)
491
+ return false;
492
+ for (const number of extractNumbers(question)) {
493
+ if (answerNumbers.has(number))
494
+ return true;
495
+ }
496
+ return false;
497
+ }
@@ -0,0 +1,23 @@
1
+ /** Поддерживаемые режимы вопроса. */
2
+ export type AnswerMode = "single" | "multi";
3
+ /** Нормализованный вариант ответа внутри predictor. */
4
+ export type AnswerOption = {
5
+ id: string;
6
+ text: string;
7
+ };
8
+ /** Evidence-фрагмент, объясняющий поддержку конкретного варианта. */
9
+ export type EvidenceItem = {
10
+ answerId: string;
11
+ page: number;
12
+ text: string;
13
+ score: number;
14
+ kind: string;
15
+ };
16
+ /** Внутренний score варианта до и после калибровки. */
17
+ export type AnswerScore = {
18
+ answer: AnswerOption;
19
+ raw: number;
20
+ evidence: EvidenceItem[];
21
+ score?: number;
22
+ relative?: number;
23
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Запускает локальный non-LLM predictor для выбора ответа.
3
+ *
4
+ * Predictor получает источник PDF, текст вопроса, варианты ответа и режим
5
+ * (`single` или `multi`). Он извлекает или переиспользует текст PDF, считает
6
+ * score для каждого варианта по документу и возвращает id выбранных ответов
7
+ * вместе с evidence-фрагментами.
8
+ *
9
+ * Runtime использует только данные, переданные вызывающим кодом.
10
+ *
11
+ * @param input Запрос с PDF-данными/путем/URL, вопросом, ответами и режимом.
12
+ * @param options Необязательные runtime-зависимости, например явный модуль PDF.js.
13
+ * @returns ID выбранных ответов, калиброванные score, raw score, evidence и метаданные.
14
+ */
15
+ export declare function predict(input: any, options?: any): Promise<{
16
+ meta: {
17
+ pageCount: any;
18
+ chunks: any;
19
+ ocrNeeded: any;
20
+ intent: {
21
+ negative: boolean;
22
+ exception: boolean;
23
+ numeric: boolean;
24
+ listLike: boolean;
25
+ };
26
+ };
27
+ diagnostics?: {
28
+ answerEvidence: {
29
+ [k: string]: any;
30
+ };
31
+ };
32
+ selected: string[];
33
+ mode: string;
34
+ confidence: number;
35
+ scores: {
36
+ [k: string]: number;
37
+ };
38
+ rawScores: {
39
+ [k: string]: number;
40
+ };
41
+ evidence: {
42
+ answerId: string;
43
+ score: number;
44
+ page: number;
45
+ text: string;
46
+ kind: string;
47
+ }[];
48
+ }>;
49
+ /**
50
+ * Очищает in-memory кеши predictor, включая кешированный текст PDF и runtime-состояние.
51
+ */
52
+ export declare function clearPredictorCache(): void;