@heripo/document-processor 0.1.47 → 0.2.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.
package/README.ko.md CHANGED
@@ -265,6 +265,8 @@ DocumentProcessor는 다음 5단계 파이프라인으로 문서를 처리합니
265
265
  - PDF 페이지와 문서 논리적 페이지 매핑
266
266
  - 스캔 오류로 인한 페이지 번호 불일치 처리
267
267
 
268
+ PDF 물리 페이지는 1부터 시작하며, 논리 페이지는 본문 1쪽을 기준으로 연속해서 매깁니다. 앞쪽은 음수와 0을 사용하며, 자동 매핑·검토된 입력·목차·챕터 배정에서 이 값을 보존합니다. 추출 실패는 내부적으로 정상 번호인 `0`과 구분합니다. 알려진 페이지의 패턴으로도 번호를 추론할 수 없으면 `PageRangeParseError`를 발생시키며, 실패한 페이지를 논리 `0`으로 출력하지 않습니다.
269
+
268
270
  ### 3. TOC 추출 (5단계 파이프라인)
269
271
 
270
272
  #### Stage 1: TocFinder (규칙 기반)
package/README.md CHANGED
@@ -280,6 +280,8 @@ DocumentProcessor processes documents through a 5-stage pipeline:
280
280
  - PDF page to document logical page mapping
281
281
  - Handle page number mismatches due to scanning errors
282
282
 
283
+ Physical PDF pages start at 1; logical pages count continuously relative to body page 1. Front matter can use negative values and zero, which are preserved in automatic mapping, reviewed inputs, TOC entries, and chapter assignment. Extraction failures are tracked separately from valid logical zero. If known page patterns cannot resolve a missing number, parsing throws `PageRangeParseError` rather than outputting logical zero for an unknown page.
284
+
283
285
  ### 3. TOC Extraction (5-Stage Pipeline)
284
286
 
285
287
  #### Stage 1: TocFinder (Rule-Based)
package/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ interface TocEntry {
19
19
  */
20
20
  level: number;
21
21
  /**
22
- * Starting page number
22
+ * Starting logical page number (signed integer, relative to body page 1)
23
23
  */
24
24
  pageNo: number;
25
25
  /**
@@ -620,7 +620,7 @@ declare enum PagePattern {
620
620
  * 2. For each group:
621
621
  * - If ≤3 pages: send all to LLM at once
622
622
  * - If >3 pages: random sample 3 pages, detect pattern, apply to all
623
- * 3. Post-process: handle drops, normalize negatives, backfill failed pages
623
+ * 3. Post-process: handle drops and infer missing logical pages, including zero and negatives
624
624
  */
625
625
  declare class PageRangeParser extends VisionLLMComponent {
626
626
  private readonly SAMPLE_SIZE;
@@ -713,11 +713,7 @@ declare class PageRangeParser extends VisionLLMComponent {
713
713
  */
714
714
  private detectAndHandleDrops;
715
715
  /**
716
- * Normalize negative page numbers to 0
717
- */
718
- private normalizeNegatives;
719
- /**
720
- * Backfill pages marked with 0 using detected pattern
716
+ * Infer unresolved pages from the logical numbering pattern
721
717
  */
722
718
  private backfillFailedPages;
723
719
  /**
@@ -1493,7 +1489,7 @@ declare class ChapterConverter {
1493
1489
  * Uses next chapter's start page as end boundary
1494
1490
  *
1495
1491
  * Front Matter (ch-000) gets special handling:
1496
- * - startPage: 1
1492
+ * - startPage: the first logical page
1497
1493
  * - endPage: first TOC entry's page - 1 (or 0 if TOC starts at page 1)
1498
1494
  */
1499
1495
  private calculatePageRanges;
@@ -1699,4 +1695,5 @@ declare class ResourceConverter {
1699
1695
  convertFootnotes(doclingDoc: DoclingDocument): ProcessedFootnote[];
1700
1696
  }
1701
1697
 
1702
- export { BaseLLMComponent, type BaseLLMComponentOptions, BaseValidator, type BaseValidatorOptions, CONTINUATION_MARKERS, CaptionParseError, CaptionParser, type CaptionParserOptions, CaptionProcessingPipeline, type CaptionProcessingPipelineDeps, type CaptionSourceExtraction, CaptionValidationError, CaptionValidator, type CaptionValidatorOptions, ChapterConverter, DocumentProcessor, type DocumentProcessorOptions, type DocumentProcessorProcessOptions, type ImageContent, PAGE_NUMBER_PATTERN, PROCESSED_DOCUMENT_SCHEMA_VERSION, PagePattern, PageRangeParseError, PageRangeParser, type PageSizeGroup, ResourceConverter, type SourceRefValidationMode, TOC_KEYWORDS, TextLLMComponent, type TocAreaResult, type TocContentValidationResult, TocContentValidationSchema, TocContentValidator, type TocContentValidatorOptions, type TocEntry, TocEntrySchema, TocExtractError, TocExtractionPipeline, type TocExtractionPipelineDeps, TocExtractor, type TocExtractorOptions, TocFinder, type TocFinderOptions, TocNotFoundError, TocParseError, type TocResponse, TocResponseSchema, VisionLLMComponent, type VisionLLMComponentOptions, type VisionTocExtractionResult, VisionTocExtractionSchema, VisionTocExtractor, type VisionTocExtractorOptions, extractMaxPageNumber };
1698
+ export { BaseLLMComponent, BaseValidator, CONTINUATION_MARKERS, CaptionParseError, CaptionParser, CaptionProcessingPipeline, CaptionValidationError, CaptionValidator, ChapterConverter, DocumentProcessor, PAGE_NUMBER_PATTERN, PROCESSED_DOCUMENT_SCHEMA_VERSION, PagePattern, PageRangeParseError, PageRangeParser, ResourceConverter, TOC_KEYWORDS, TextLLMComponent, TocContentValidationSchema, TocContentValidator, TocEntrySchema, TocExtractError, TocExtractionPipeline, TocExtractor, TocFinder, TocNotFoundError, TocParseError, TocResponseSchema, VisionLLMComponent, VisionTocExtractionSchema, VisionTocExtractor, extractMaxPageNumber };
1699
+ export type { BaseLLMComponentOptions, BaseValidatorOptions, CaptionParserOptions, CaptionProcessingPipelineDeps, CaptionSourceExtraction, CaptionValidatorOptions, DocumentProcessorOptions, DocumentProcessorProcessOptions, ImageContent, PageSizeGroup, SourceRefValidationMode, TocAreaResult, TocContentValidationResult, TocContentValidatorOptions, TocEntry, TocExtractionPipelineDeps, TocExtractorOptions, TocFinderOptions, TocResponse, VisionLLMComponentOptions, VisionTocExtractionResult, VisionTocExtractorOptions };
package/dist/index.js CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  hasToolCall,
7
7
  tool
8
8
  } from "ai";
9
+ import { wrapLanguageModel } from "ai";
10
+ import { wrapLanguageModel as wrapLanguageModel2 } from "ai";
9
11
  var BatchProcessor = class {
10
12
  /**
11
13
  * Splits an array into batches of specified size.
@@ -135,9 +137,23 @@ var LLMCaller = class {
135
137
  cacheWriteTokens: allWrites - (write1h ?? 0)
136
138
  },
137
139
  ...write1h !== void 0 && { cacheWrite1hTokens: write1h },
140
+ ...usage?.outputTokenDetails?.reasoningTokens != null && {
141
+ reasoningTokens: usage.outputTokenDetails.reasoningTokens
142
+ },
138
143
  metadata: config.metadata
139
144
  };
140
145
  }
146
+ /** Finish reason and structured-output attempts, when known. */
147
+ static callDetails(response) {
148
+ return {
149
+ ...response.finishReason !== void 0 && {
150
+ finishReason: response.finishReason
151
+ },
152
+ ...response.structuredAttempts !== void 0 && {
153
+ structuredAttempts: response.structuredAttempts
154
+ }
155
+ };
156
+ }
141
157
  /**
142
158
  * Maximum number of retries when structured output generation fails.
143
159
  * Total attempts = MAX_STRUCTURED_OUTPUT_RETRIES + 1.
@@ -178,7 +194,9 @@ var LLMCaller = class {
178
194
  return {
179
195
  output: toolCall.input,
180
196
  usage: lastResult.usage,
181
- steps: lastResult.steps
197
+ steps: lastResult.steps,
198
+ finishReason: lastResult.finishReason,
199
+ structuredAttempts: attempt + 1
182
200
  };
183
201
  }
184
202
  }
@@ -212,11 +230,18 @@ var LLMCaller = class {
212
230
  let lastError;
213
231
  for (let attempt = 0; attempt <= this.MAX_STRUCTURED_OUTPUT_RETRIES; attempt++) {
214
232
  try {
215
- return await generateText({
233
+ const result = await generateText({
216
234
  model,
217
235
  output: Output.object({ schema }),
218
236
  ...promptParams
219
237
  });
238
+ return {
239
+ output: result.output,
240
+ usage: result.usage,
241
+ steps: result.steps,
242
+ finishReason: result.finishReason,
243
+ structuredAttempts: attempt + 1
244
+ };
220
245
  } catch (error) {
221
246
  if (NoObjectGeneratedError.isInstance(error)) {
222
247
  lastError = error;
@@ -240,7 +265,8 @@ var LLMCaller = class {
240
265
  return {
241
266
  output: response.output,
242
267
  usage: this.buildUsage(config, primaryModelName, response, false),
243
- usedFallback: false
268
+ usedFallback: false,
269
+ ...this.callDetails(response)
244
270
  };
245
271
  } catch (primaryError) {
246
272
  if (config.abortSignal?.aborted) {
@@ -254,7 +280,8 @@ var LLMCaller = class {
254
280
  return {
255
281
  output: response.output,
256
282
  usage: this.buildUsage(config, fallbackModelName, response, true),
257
- usedFallback: true
283
+ usedFallback: true,
284
+ ...this.callDetails(response)
258
285
  };
259
286
  }
260
287
  }
@@ -277,8 +304,17 @@ var LLMCaller = class {
277
304
  return this.executeWithFallback(
278
305
  config,
279
306
  (model) => this.generateStructuredOutput(model, config.schema, {
280
- instructions: config.systemPrompt,
307
+ instructions: config.cacheSystemPrompt ? {
308
+ role: "system",
309
+ content: config.systemPrompt,
310
+ providerOptions: {
311
+ anthropic: { cacheControl: { type: "ephemeral" } }
312
+ }
313
+ } : config.systemPrompt,
281
314
  prompt: config.userPrompt,
315
+ ...config.providerOptions && {
316
+ providerOptions: config.providerOptions
317
+ },
282
318
  temperature: config.temperature,
283
319
  maxRetries: config.maxRetries,
284
320
  abortSignal: config.abortSignal
@@ -548,6 +584,7 @@ var LLMTokenUsageAggregator = class {
548
584
  this.usage = {};
549
585
  }
550
586
  };
587
+ var encoder = new TextEncoder();
551
588
 
552
589
  // src/utils/ref-resolver.ts
553
590
  var RefResolver = class {
@@ -1037,7 +1074,7 @@ var ChapterConverter = class _ChapterConverter {
1037
1074
  */
1038
1075
  convert(tocEntries, textItems, pageRangeMap, images, tables, footnotes) {
1039
1076
  this.logger.info("[ChapterConverter] Starting chapter conversion...");
1040
- const frontMatter = this.createFrontMatterChapter();
1077
+ const frontMatter = this.createFrontMatterChapter(pageRangeMap);
1041
1078
  const tocChapters = this.buildChapterTree(tocEntries);
1042
1079
  this.logger.info(
1043
1080
  `[ChapterConverter] Built ${tocChapters.length} TOC chapters + Front Matter`
@@ -1069,12 +1106,15 @@ var ChapterConverter = class _ChapterConverter {
1069
1106
  /**
1070
1107
  * Create Front Matter chapter for pre-TOC content
1071
1108
  */
1072
- createFrontMatterChapter() {
1109
+ createFrontMatterChapter(pageRangeMap) {
1110
+ const logicalPages = Object.values(pageRangeMap).map(
1111
+ (range) => range.startPageNo
1112
+ );
1073
1113
  return {
1074
1114
  id: _ChapterConverter.FRONT_MATTER_ID,
1075
1115
  originTitle: _ChapterConverter.FRONT_MATTER_TITLE,
1076
1116
  title: _ChapterConverter.FRONT_MATTER_TITLE,
1077
- pageNo: 1,
1117
+ pageNo: logicalPages.length > 0 ? Math.min(...logicalPages) : 1,
1078
1118
  level: 1,
1079
1119
  sourceRefs: [],
1080
1120
  textBlocks: [],
@@ -1133,7 +1173,7 @@ var ChapterConverter = class _ChapterConverter {
1133
1173
  * Uses next chapter's start page as end boundary
1134
1174
  *
1135
1175
  * Front Matter (ch-000) gets special handling:
1136
- * - startPage: 1
1176
+ * - startPage: the first logical page
1137
1177
  * - endPage: first TOC entry's page - 1 (or 0 if TOC starts at page 1)
1138
1178
  */
1139
1179
  calculatePageRanges(flatChapters, tocEntries) {
@@ -1147,7 +1187,7 @@ var ChapterConverter = class _ChapterConverter {
1147
1187
  );
1148
1188
  const sorted = [...tocChapters].sort((a, b) => a.tocPageNo - b.tocPageNo);
1149
1189
  ranges.set(_ChapterConverter.FRONT_MATTER_ID, {
1150
- startPage: 1,
1190
+ startPage: flatChapters[0].tocPageNo,
1151
1191
  endPage: firstTocPage - 1
1152
1192
  });
1153
1193
  sorted.forEach((current, i) => {
@@ -1211,7 +1251,7 @@ var ChapterConverter = class _ChapterConverter {
1211
1251
  */
1212
1252
  findChapterForPage(actualPageNo, chapterRanges) {
1213
1253
  let bestMatch = null;
1214
- let bestStartPage = -1;
1254
+ let bestStartPage = -Infinity;
1215
1255
  for (const [chapterId, range] of chapterRanges) {
1216
1256
  if (actualPageNo >= range.startPage && actualPageNo <= range.endPage) {
1217
1257
  if (range.startPage > bestStartPage) {
@@ -1612,7 +1652,7 @@ ${details}`,
1612
1652
  * Recursively validate entries
1613
1653
  */
1614
1654
  validateEntries(entries, parentPath, parentEntry, seenKeys) {
1615
- let prevPageNo = parentEntry?.pageNo ?? 0;
1655
+ let prevPageNo = parentEntry?.pageNo ?? -Infinity;
1616
1656
  for (let i = 0; i < entries.length; i++) {
1617
1657
  const entry = entries[i];
1618
1658
  const path4 = parentPath ? `${parentPath}.children[${i}]` : `[${i}]`;
@@ -1662,10 +1702,10 @@ ${details}`,
1662
1702
  * V002: Validate page number range
1663
1703
  */
1664
1704
  validatePageRange(entry, path4) {
1665
- if (entry.pageNo < 1) {
1705
+ if (!Number.isInteger(entry.pageNo)) {
1666
1706
  this.addIssue({
1667
1707
  code: "V002",
1668
- message: `Page number must be >= 1, got ${entry.pageNo}`,
1708
+ message: `Page number must be an integer, got ${entry.pageNo}`,
1669
1709
  path: path4,
1670
1710
  entry
1671
1711
  });
@@ -2242,7 +2282,7 @@ var TextLLMComponent = class extends BaseLLMComponent {
2242
2282
  var DEFAULT_MAX_VALIDATION_RETRIES = 3;
2243
2283
  var VALIDATION_CODE_DESCRIPTIONS = {
2244
2284
  V001: "Page numbers must be in non-decreasing order within the same level. A decrease usually means a hierarchy or page number error.",
2245
- V002: "Page number is out of valid range (must be >= 1 and <= total pages).",
2285
+ V002: "Page number must be an integer within the configured logical page range.",
2246
2286
  V003: "Title is empty or contains only whitespace.",
2247
2287
  V004: "Title exceeds the maximum allowed length.",
2248
2288
  V005: "Child page number is before parent page number. Children must start on or after the parent page.",
@@ -2253,7 +2293,7 @@ function buildTocEntrySchema(maxDepth) {
2253
2293
  const base = {
2254
2294
  title: z.string().describe("Chapter or section title"),
2255
2295
  level: z.number().int().min(1).describe("Hierarchy depth (1 = top level)"),
2256
- pageNo: z.number().int().min(1).describe("Starting page number")
2296
+ pageNo: z.number().int().describe("Starting logical page number")
2257
2297
  };
2258
2298
  if (maxDepth <= 0) {
2259
2299
  return z.object(base);
@@ -3180,7 +3220,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3180
3220
  "info",
3181
3221
  `Found ${sizeGroups.length} size group(s), total ${pages.length} pages`
3182
3222
  );
3183
- const pageRangeMap = {};
3223
+ const extractedPageRanges = {};
3184
3224
  const usageList = [];
3185
3225
  for (let i = 0; i < sizeGroups.length; i++) {
3186
3226
  const group = sizeGroups[i];
@@ -3189,11 +3229,20 @@ var PageRangeParser = class extends VisionLLMComponent {
3189
3229
  `Processing group ${i + 1}/${sizeGroups.length}: ${group.pageNos.length} pages`
3190
3230
  );
3191
3231
  const groupResult = await this.processGroup(pages, group, this.model);
3192
- Object.assign(pageRangeMap, groupResult.pageRangeMap);
3232
+ Object.assign(extractedPageRanges, groupResult.pageRangeMap);
3193
3233
  usageList.push(...groupResult.usage);
3194
3234
  }
3195
3235
  usageList.forEach((usage) => this.trackUsage(usage));
3196
- this.postProcess(pageRangeMap);
3236
+ this.postProcess(extractedPageRanges);
3237
+ const pageRangeMap = {};
3238
+ for (const [pdfPage, range] of Object.entries(extractedPageRanges)) {
3239
+ if (range === null) {
3240
+ throw new PageRangeParseError(
3241
+ `Could not determine a logical page number for PDF page ${pdfPage}`
3242
+ );
3243
+ }
3244
+ pageRangeMap[Number(pdfPage)] = range;
3245
+ }
3197
3246
  this.log(
3198
3247
  "info",
3199
3248
  `Completed: ${Object.keys(pageRangeMap).length} pages mapped`
@@ -3321,7 +3370,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3321
3370
  `Could not detect page pattern after ${consecutiveAllNullAttempts >= MAX_ALL_NULL_ATTEMPTS ? consecutiveAllNullAttempts : this.MAX_PATTERN_RETRIES + 1} attempts for group with ${pageNos.length} pages, marking as failed`
3322
3371
  );
3323
3372
  const fallbackMap = Object.fromEntries(
3324
- pageNos.map((pageNo) => [pageNo, { startPageNo: 0, endPageNo: 0 }])
3373
+ pageNos.map((pageNo) => [pageNo, null])
3325
3374
  );
3326
3375
  return { pageRangeMap: fallbackMap, usage: usageList };
3327
3376
  }
@@ -3363,8 +3412,8 @@ var PageRangeParser = class extends VisionLLMComponent {
3363
3412
  pages: z4.array(
3364
3413
  z4.object({
3365
3414
  imageIndex: z4.number().describe("0-based index of the image in the request"),
3366
- startPageNo: z4.number().nullable().describe("Start page number (null if not found)"),
3367
- endPageNo: z4.number().nullable().describe(
3415
+ startPageNo: z4.number().int().nullable().describe("Start page number (null if not found)"),
3416
+ endPageNo: z4.number().int().nullable().describe(
3368
3417
  "End page number for double-sided scans (null for single page)"
3369
3418
  )
3370
3419
  })
@@ -3477,7 +3526,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3477
3526
  break;
3478
3527
  }
3479
3528
  default:
3480
- result[pdfPageNo] = { startPageNo: 0, endPageNo: 0 };
3529
+ result[pdfPageNo] = null;
3481
3530
  }
3482
3531
  });
3483
3532
  return result;
@@ -3492,7 +3541,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3492
3541
  sample.startPageNo !== null ? {
3493
3542
  startPageNo: sample.startPageNo,
3494
3543
  endPageNo: sample.endPageNo ?? sample.startPageNo
3495
- } : { startPageNo: 0, endPageNo: 0 }
3544
+ } : null
3496
3545
  ])
3497
3546
  );
3498
3547
  }
@@ -3502,7 +3551,6 @@ var PageRangeParser = class extends VisionLLMComponent {
3502
3551
  postProcess(pageRangeMap) {
3503
3552
  this.detectAndHandleOutliers(pageRangeMap);
3504
3553
  this.detectAndHandleDrops(pageRangeMap);
3505
- this.normalizeNegatives(pageRangeMap);
3506
3554
  this.backfillFailedPages(pageRangeMap);
3507
3555
  }
3508
3556
  /**
@@ -3528,8 +3576,9 @@ var PageRangeParser = class extends VisionLLMComponent {
3528
3576
  let hasOutliers = false;
3529
3577
  for (let i = 0; i < normalSequenceStart; i++) {
3530
3578
  const pdfPage = pdfPages[i];
3531
- const pageNo = pageRangeMap[pdfPage].startPageNo;
3532
- if (pageNo === 0) continue;
3579
+ const range = pageRangeMap[pdfPage];
3580
+ if (range === null) continue;
3581
+ const pageNo = range.startPageNo;
3533
3582
  const pdfDiff = normalStartPdfPage - pdfPage;
3534
3583
  const isDoubleSided = this.isDoubleSidedRange(
3535
3584
  pageRangeMap[normalStartPdfPage]
@@ -3540,7 +3589,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3540
3589
  "info",
3541
3590
  `Outlier detected: PDF ${pdfPage}=${pageNo} (expected ~${expectedPageNo})`
3542
3591
  );
3543
- pageRangeMap[pdfPage] = { startPageNo: 0, endPageNo: 0 };
3592
+ pageRangeMap[pdfPage] = null;
3544
3593
  hasOutliers = true;
3545
3594
  }
3546
3595
  }
@@ -3567,7 +3616,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3567
3616
  const nextPdfPage = pdfPages[startIdx + i + 1];
3568
3617
  const currRange = pageRangeMap[currPdfPage];
3569
3618
  const nextRange = pageRangeMap[nextPdfPage];
3570
- if (currRange.startPageNo === 0 || nextRange.startPageNo === 0) {
3619
+ if (currRange === null || nextRange === null) {
3571
3620
  isValidSequence = false;
3572
3621
  break;
3573
3622
  }
@@ -3608,40 +3657,32 @@ var PageRangeParser = class extends VisionLLMComponent {
3608
3657
  for (let i = 1; i < pdfPages.length; i++) {
3609
3658
  const prevPdfPage = pdfPages[i - 1];
3610
3659
  const currPdfPage = pdfPages[i];
3611
- const prevPageNo = pageRangeMap[prevPdfPage].startPageNo;
3612
- const currPageNo = pageRangeMap[currPdfPage].startPageNo;
3613
- if (prevPageNo === 0 || currPageNo === 0) continue;
3614
- if (currPageNo > 0 && prevPageNo > currPageNo && prevPageNo - currPageNo > 1) {
3660
+ const prevRange = pageRangeMap[prevPdfPage];
3661
+ const currRange = pageRangeMap[currPdfPage];
3662
+ if (prevRange === null || currRange === null) continue;
3663
+ const prevPageNo = prevRange.startPageNo;
3664
+ const currPageNo = currRange.startPageNo;
3665
+ if (prevPageNo > currPageNo && prevPageNo - currPageNo > 1) {
3615
3666
  this.log(
3616
3667
  "info",
3617
3668
  `Page drop detected: PDF ${prevPdfPage}=${prevPageNo} -> PDF ${currPdfPage}=${currPageNo}`
3618
3669
  );
3619
- const isDoubleSided = this.isDoubleSidedRange(
3620
- pageRangeMap[currPdfPage]
3621
- );
3670
+ const isDoubleSided = this.isDoubleSidedRange(currRange);
3622
3671
  for (let j = i - 1; j >= 0; j--) {
3623
3672
  const pdfPage = pdfPages[j];
3624
3673
  const distance = currPdfPage - pdfPage;
3625
3674
  if (isDoubleSided) {
3626
3675
  const expectedStartPageNo = currPageNo - distance * 2;
3627
- if (expectedStartPageNo < 1) {
3628
- pageRangeMap[pdfPage] = { startPageNo: 0, endPageNo: 0 };
3629
- } else {
3630
- pageRangeMap[pdfPage] = {
3631
- startPageNo: expectedStartPageNo,
3632
- endPageNo: expectedStartPageNo + 1
3633
- };
3634
- }
3676
+ pageRangeMap[pdfPage] = {
3677
+ startPageNo: expectedStartPageNo,
3678
+ endPageNo: expectedStartPageNo + 1
3679
+ };
3635
3680
  } else {
3636
3681
  const expectedPageNo = currPageNo - distance;
3637
- if (expectedPageNo < 1) {
3638
- pageRangeMap[pdfPage] = { startPageNo: 0, endPageNo: 0 };
3639
- } else {
3640
- pageRangeMap[pdfPage] = {
3641
- startPageNo: expectedPageNo,
3642
- endPageNo: expectedPageNo
3643
- };
3644
- }
3682
+ pageRangeMap[pdfPage] = {
3683
+ startPageNo: expectedPageNo,
3684
+ endPageNo: expectedPageNo
3685
+ };
3645
3686
  }
3646
3687
  this.log(
3647
3688
  "info",
@@ -3652,32 +3693,19 @@ var PageRangeParser = class extends VisionLLMComponent {
3652
3693
  }
3653
3694
  }
3654
3695
  /**
3655
- * Normalize negative page numbers to 0
3656
- */
3657
- normalizeNegatives(pageRangeMap) {
3658
- for (const [pdfPageStr, range] of Object.entries(pageRangeMap)) {
3659
- if (range.startPageNo < 0 || range.endPageNo < 0) {
3660
- this.log("info", `Normalizing negative: PDF ${pdfPageStr} -> 0`);
3661
- pageRangeMap[Number(pdfPageStr)] = { startPageNo: 0, endPageNo: 0 };
3662
- }
3663
- }
3664
- }
3665
- /**
3666
- * Backfill pages marked with 0 using detected pattern
3696
+ * Infer unresolved pages from the logical numbering pattern
3667
3697
  */
3668
3698
  backfillFailedPages(pageRangeMap) {
3669
3699
  const pdfPages = Object.keys(pageRangeMap).map(Number).sort((a, b) => a - b);
3670
- const failedPages = pdfPages.filter(
3671
- (p) => pageRangeMap[p].startPageNo === 0
3672
- );
3700
+ const failedPages = pdfPages.filter((p) => pageRangeMap[p] === null);
3673
3701
  if (failedPages.length === 0) return;
3674
- const successfulPages = pdfPages.filter((p) => pageRangeMap[p].startPageNo > 0).map((p) => ({
3702
+ const successfulPages = pdfPages.filter((p) => pageRangeMap[p] !== null).map((p) => ({
3675
3703
  pdfPage: p,
3676
3704
  pageNo: pageRangeMap[p].startPageNo,
3677
3705
  isDoubleSided: this.isDoubleSidedRange(pageRangeMap[p])
3678
3706
  }));
3679
- if (successfulPages.length < 2) {
3680
- this.log("warn", "Not enough successful pages for backfill");
3707
+ if (successfulPages.length === 0) {
3708
+ this.log("warn", "No known logical pages for backfill");
3681
3709
  return;
3682
3710
  }
3683
3711
  const doubleSidedCount = successfulPages.filter(
@@ -3695,16 +3723,9 @@ var PageRangeParser = class extends VisionLLMComponent {
3695
3723
  );
3696
3724
  for (const pdfPage of failedPages) {
3697
3725
  const expectedStartPageNo = pdfPage * 2 + avgOffset;
3698
- if (expectedStartPageNo < 1) {
3699
- this.log(
3700
- "info",
3701
- `Backfill skipped for PDF ${pdfPage} (would be ${expectedStartPageNo})`
3702
- );
3703
- continue;
3704
- }
3705
3726
  this.log(
3706
3727
  "info",
3707
- `Backfill PDF ${pdfPage}: 0 -> ${expectedStartPageNo}-${expectedStartPageNo + 1}`
3728
+ `Backfill PDF ${pdfPage}: unknown -> ${expectedStartPageNo}-${expectedStartPageNo + 1}`
3708
3729
  );
3709
3730
  pageRangeMap[pdfPage] = {
3710
3731
  startPageNo: expectedStartPageNo,
@@ -3722,14 +3743,10 @@ var PageRangeParser = class extends VisionLLMComponent {
3722
3743
  );
3723
3744
  for (const pdfPage of failedPages) {
3724
3745
  const expectedPageNo = pdfPage + avgOffset;
3725
- if (expectedPageNo < 1) {
3726
- this.log(
3727
- "info",
3728
- `Backfill skipped for PDF ${pdfPage} (would be ${expectedPageNo})`
3729
- );
3730
- continue;
3731
- }
3732
- this.log("info", `Backfill PDF ${pdfPage}: 0 -> ${expectedPageNo}`);
3746
+ this.log(
3747
+ "info",
3748
+ `Backfill PDF ${pdfPage}: unknown -> ${expectedPageNo}`
3749
+ );
3733
3750
  pageRangeMap[pdfPage] = {
3734
3751
  startPageNo: expectedPageNo,
3735
3752
  endPageNo: expectedPageNo