@heripo/document-processor 0.1.16 → 0.1.18

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/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  CONTINUATION_MARKERS: () => CONTINUATION_MARKERS,
36
36
  CaptionParseError: () => CaptionParseError,
37
37
  CaptionParser: () => CaptionParser,
38
+ CaptionProcessingPipeline: () => CaptionProcessingPipeline,
38
39
  CaptionValidationError: () => CaptionValidationError,
39
40
  CaptionValidator: () => CaptionValidator,
40
41
  ChapterConverter: () => ChapterConverter,
@@ -43,12 +44,14 @@ __export(index_exports, {
43
44
  PagePattern: () => PagePattern,
44
45
  PageRangeParseError: () => PageRangeParseError,
45
46
  PageRangeParser: () => PageRangeParser,
47
+ ResourceConverter: () => ResourceConverter,
46
48
  TOC_KEYWORDS: () => TOC_KEYWORDS,
47
49
  TextLLMComponent: () => TextLLMComponent,
48
50
  TocContentValidationSchema: () => TocContentValidationSchema,
49
51
  TocContentValidator: () => TocContentValidator,
50
52
  TocEntrySchema: () => TocEntrySchema,
51
53
  TocExtractError: () => TocExtractError,
54
+ TocExtractionPipeline: () => TocExtractionPipeline,
52
55
  TocExtractor: () => TocExtractor,
53
56
  TocFinder: () => TocFinder,
54
57
  TocNotFoundError: () => TocNotFoundError,
@@ -56,7 +59,8 @@ __export(index_exports, {
56
59
  TocResponseSchema: () => TocResponseSchema,
57
60
  VisionLLMComponent: () => VisionLLMComponent,
58
61
  VisionTocExtractionSchema: () => VisionTocExtractionSchema,
59
- VisionTocExtractor: () => VisionTocExtractor
62
+ VisionTocExtractor: () => VisionTocExtractor,
63
+ extractMaxPageNumber: () => extractMaxPageNumber
60
64
  });
61
65
  module.exports = __toCommonJS(index_exports);
62
66
 
@@ -605,11 +609,7 @@ var RefResolver = class {
605
609
  * Build an index mapping self_ref to the actual item
606
610
  */
607
611
  buildIndex(items, _prefix) {
608
- const map = /* @__PURE__ */ new Map();
609
- for (const item of items) {
610
- map.set(item.self_ref, item);
611
- }
612
- return map;
612
+ return new Map(items.map((item) => [item.self_ref, item]));
613
613
  }
614
614
  /**
615
615
  * Resolve a $ref string to the actual item
@@ -835,14 +835,7 @@ var TextCleaner = class {
835
835
  */
836
836
  static normalizeAndFilterBatch(texts, batchSize = 10) {
837
837
  if (batchSize === 0) {
838
- const results = [];
839
- for (const text of texts) {
840
- const normalized = this.normalize(text);
841
- if (this.isValidText(normalized)) {
842
- results.push(normalized);
843
- }
844
- }
845
- return results;
838
+ return texts.map((text) => this.normalize(text)).filter((normalized) => this.isValidText(normalized));
846
839
  }
847
840
  return BatchProcessor.processBatchSync(texts, batchSize, (batch) => {
848
841
  const normalized = this.normalizeBatch(batch);
@@ -864,38 +857,22 @@ var MarkdownConverter = class _MarkdownConverter {
864
857
  if (refs.length === 0) {
865
858
  return "";
866
859
  }
867
- const lines = [];
868
- for (const ref of refs) {
869
- const item = refResolver.resolve(ref);
870
- if (!item) {
871
- continue;
872
- }
860
+ const lines = refs.map((ref) => refResolver.resolve(ref)).filter((item) => item !== null).map((item) => {
873
861
  if ("name" in item && (item.name === "list" || item.name === "group")) {
874
- const groupMarkdown = _MarkdownConverter.groupToMarkdown(
862
+ return _MarkdownConverter.groupToMarkdown(
875
863
  item,
876
864
  refResolver,
877
865
  0
878
866
  );
879
- if (groupMarkdown) {
880
- lines.push(groupMarkdown);
881
- }
882
- } else if ("data" in item && "grid" in item.data) {
883
- const tableMarkdown = _MarkdownConverter.tableToMarkdown(
884
- item
885
- );
886
- if (tableMarkdown) {
887
- lines.push(tableMarkdown);
888
- }
889
- } else if ("text" in item && "orig" in item) {
890
- const textMarkdown = _MarkdownConverter.textToMarkdown(
891
- item,
892
- 0
893
- );
894
- if (textMarkdown) {
895
- lines.push(textMarkdown);
896
- }
897
867
  }
898
- }
868
+ if ("data" in item && "grid" in item.data) {
869
+ return _MarkdownConverter.tableToMarkdown(item);
870
+ }
871
+ if ("text" in item && "orig" in item) {
872
+ return _MarkdownConverter.textToMarkdown(item, 0);
873
+ }
874
+ return "";
875
+ }).filter((line) => line !== "");
899
876
  return lines.join("\n\n");
900
877
  }
901
878
  /**
@@ -911,31 +888,22 @@ var MarkdownConverter = class _MarkdownConverter {
911
888
  * - Chapter 2 Methodology ..... 10
912
889
  */
913
890
  static groupToMarkdown(group, refResolver, indentLevel = 0) {
914
- const lines = [];
915
- for (const childRef of group.children) {
916
- const child = refResolver.resolve(childRef.$ref);
917
- if (!child) {
918
- continue;
919
- }
891
+ const lines = group.children.map((childRef) => refResolver.resolve(childRef.$ref)).filter((child) => child !== null).map((child) => {
920
892
  if ("name" in child && (child.name === "list" || child.name === "group")) {
921
- const nestedMarkdown = _MarkdownConverter.groupToMarkdown(
893
+ return _MarkdownConverter.groupToMarkdown(
922
894
  child,
923
895
  refResolver,
924
896
  indentLevel + 1
925
897
  );
926
- if (nestedMarkdown) {
927
- lines.push(nestedMarkdown);
928
- }
929
- } else if ("text" in child && "orig" in child) {
930
- const textMarkdown = _MarkdownConverter.textToMarkdown(
898
+ }
899
+ if ("text" in child && "orig" in child) {
900
+ return _MarkdownConverter.textToMarkdown(
931
901
  child,
932
902
  indentLevel
933
903
  );
934
- if (textMarkdown) {
935
- lines.push(textMarkdown);
936
- }
937
904
  }
938
- }
905
+ return "";
906
+ }).filter((line) => line !== "");
939
907
  return lines.join("\n");
940
908
  }
941
909
  /**
@@ -953,21 +921,20 @@ var MarkdownConverter = class _MarkdownConverter {
953
921
  if (!grid || grid.length === 0) {
954
922
  return "";
955
923
  }
956
- const lines = [];
957
- for (let rowIdx = 0; rowIdx < grid.length; rowIdx++) {
958
- const row = grid[rowIdx];
924
+ const lines = grid.flatMap((row, rowIdx) => {
959
925
  if (!row || row.length === 0) {
960
- continue;
926
+ return [];
961
927
  }
962
928
  const cells = row.map(
963
929
  (cell) => _MarkdownConverter.escapeTableCell(cell.text)
964
930
  );
965
- lines.push(`| ${cells.join(" | ")} |`);
931
+ const rowLine = `| ${cells.join(" | ")} |`;
966
932
  if (rowIdx === 0) {
967
933
  const separator = row.map(() => "---").join(" | ");
968
- lines.push(`| ${separator} |`);
934
+ return [rowLine, `| ${separator} |`];
969
935
  }
970
- }
936
+ return [rowLine];
937
+ });
971
938
  return lines.join("\n");
972
939
  }
973
940
  /**
@@ -1014,6 +981,20 @@ var MarkdownConverter = class _MarkdownConverter {
1014
981
  }
1015
982
  };
1016
983
 
984
+ // src/utils/toc-markdown-utils.ts
985
+ function extractMaxPageNumber(markdown) {
986
+ const dotLeaderMatches = [...markdown.matchAll(/\.{2,}\s*(\d+)/g)];
987
+ const tableCellMatches = [...markdown.matchAll(/\|\s*(\d+)\s*\|\s*$/gm)];
988
+ const allNumbers = [
989
+ ...dotLeaderMatches.map((m) => parseInt(m[1], 10)),
990
+ ...tableCellMatches.map((m) => parseInt(m[1], 10))
991
+ ];
992
+ if (allNumbers.length === 0) {
993
+ return 0;
994
+ }
995
+ return Math.max(...allNumbers);
996
+ }
997
+
1017
998
  // src/converters/chapter-converter.ts
1018
999
  var ChapterConverter = class _ChapterConverter {
1019
1000
  static FRONT_MATTER_ID = "ch-000";
@@ -1113,7 +1094,7 @@ var ChapterConverter = class _ChapterConverter {
1113
1094
  flattenChapters(chapters) {
1114
1095
  const result = [];
1115
1096
  const flatten = (chapterList) => {
1116
- for (const chapter of chapterList) {
1097
+ chapterList.forEach((chapter) => {
1117
1098
  result.push({
1118
1099
  chapter,
1119
1100
  tocPageNo: chapter.pageNo
@@ -1121,7 +1102,7 @@ var ChapterConverter = class _ChapterConverter {
1121
1102
  if (chapter.children && chapter.children.length > 0) {
1122
1103
  flatten(chapter.children);
1123
1104
  }
1124
- }
1105
+ });
1125
1106
  };
1126
1107
  flatten(chapters);
1127
1108
  return result;
@@ -1148,14 +1129,13 @@ var ChapterConverter = class _ChapterConverter {
1148
1129
  startPage: 1,
1149
1130
  endPage: firstTocPage - 1
1150
1131
  });
1151
- for (let i = 0; i < sorted.length; i++) {
1152
- const current = sorted[i];
1132
+ sorted.forEach((current, i) => {
1153
1133
  const next = sorted[i + 1];
1154
1134
  ranges.set(current.chapter.id, {
1155
1135
  startPage: current.tocPageNo,
1156
1136
  endPage: next ? next.tocPageNo - 1 : Number.MAX_SAFE_INTEGER
1157
1137
  });
1158
- }
1138
+ });
1159
1139
  return ranges;
1160
1140
  }
1161
1141
  /**
@@ -1224,7 +1204,7 @@ var ChapterConverter = class _ChapterConverter {
1224
1204
  */
1225
1205
  assignTextBlocks(chapters, textBlocks, chapterRanges, pageRangeMap) {
1226
1206
  const chapterMap = this.buildChapterMap(chapters);
1227
- for (const textBlock of textBlocks) {
1207
+ textBlocks.forEach((textBlock) => {
1228
1208
  const actualPageNo = this.pdfPageToActualPage(
1229
1209
  textBlock.pdfPageNo,
1230
1210
  pageRangeMap
@@ -1233,14 +1213,14 @@ var ChapterConverter = class _ChapterConverter {
1233
1213
  if (chapterId && chapterMap.has(chapterId)) {
1234
1214
  chapterMap.get(chapterId).textBlocks.push(textBlock);
1235
1215
  }
1236
- }
1216
+ });
1237
1217
  }
1238
1218
  /**
1239
1219
  * Link images, tables, and footnotes to chapters based on page ranges
1240
1220
  */
1241
1221
  linkResources(chapters, images, tables, footnotes, chapterRanges, pageRangeMap) {
1242
1222
  const chapterMap = this.buildChapterMap(chapters);
1243
- for (const image of images) {
1223
+ images.forEach((image) => {
1244
1224
  const actualPageNo = this.pdfPageToActualPage(
1245
1225
  image.pdfPageNo,
1246
1226
  pageRangeMap
@@ -1249,8 +1229,8 @@ var ChapterConverter = class _ChapterConverter {
1249
1229
  if (chapterId && chapterMap.has(chapterId)) {
1250
1230
  chapterMap.get(chapterId).imageIds.push(image.id);
1251
1231
  }
1252
- }
1253
- for (const table of tables) {
1232
+ });
1233
+ tables.forEach((table) => {
1254
1234
  const actualPageNo = this.pdfPageToActualPage(
1255
1235
  table.pdfPageNo,
1256
1236
  pageRangeMap
@@ -1259,8 +1239,8 @@ var ChapterConverter = class _ChapterConverter {
1259
1239
  if (chapterId && chapterMap.has(chapterId)) {
1260
1240
  chapterMap.get(chapterId).tableIds.push(table.id);
1261
1241
  }
1262
- }
1263
- for (const footnote of footnotes) {
1242
+ });
1243
+ footnotes.forEach((footnote) => {
1264
1244
  const actualPageNo = this.pdfPageToActualPage(
1265
1245
  footnote.pdfPageNo,
1266
1246
  pageRangeMap
@@ -1269,7 +1249,7 @@ var ChapterConverter = class _ChapterConverter {
1269
1249
  if (chapterId && chapterMap.has(chapterId)) {
1270
1250
  chapterMap.get(chapterId).footnoteIds.push(footnote.id);
1271
1251
  }
1272
- }
1252
+ });
1273
1253
  }
1274
1254
  /**
1275
1255
  * Build flat chapter map for O(1) lookup
@@ -1277,18 +1257,135 @@ var ChapterConverter = class _ChapterConverter {
1277
1257
  buildChapterMap(chapters) {
1278
1258
  const map = /* @__PURE__ */ new Map();
1279
1259
  const addToMap = (chapterList) => {
1280
- for (const chapter of chapterList) {
1260
+ chapterList.forEach((chapter) => {
1281
1261
  map.set(chapter.id, chapter);
1282
1262
  if (chapter.children && chapter.children.length > 0) {
1283
1263
  addToMap(chapter.children);
1284
1264
  }
1285
- }
1265
+ });
1286
1266
  };
1287
1267
  addToMap(chapters);
1288
1268
  return map;
1289
1269
  }
1290
1270
  };
1291
1271
 
1272
+ // src/converters/resource-converter.ts
1273
+ var ResourceConverter = class {
1274
+ logger;
1275
+ idGenerator;
1276
+ captionProcessingPipeline;
1277
+ constructor(logger, idGenerator, captionProcessingPipeline) {
1278
+ this.logger = logger;
1279
+ this.idGenerator = idGenerator;
1280
+ this.captionProcessingPipeline = captionProcessingPipeline;
1281
+ }
1282
+ /**
1283
+ * Convert all resources (images, tables, footnotes)
1284
+ *
1285
+ * Runs image and table conversions in parallel, then footnotes synchronously.
1286
+ */
1287
+ async convertAll(doclingDoc, outputPath) {
1288
+ this.logger.info(
1289
+ "[ResourceConverter] Converting images, tables, and footnotes..."
1290
+ );
1291
+ const [images, tables] = await Promise.all([
1292
+ this.convertImages(doclingDoc, outputPath),
1293
+ this.convertTables(doclingDoc)
1294
+ ]);
1295
+ const footnotes = this.convertFootnotes(doclingDoc);
1296
+ this.logger.info(
1297
+ `[ResourceConverter] Converted ${images.length} images, ${tables.length} tables, and ${footnotes.length} footnotes`
1298
+ );
1299
+ return { images, tables, footnotes };
1300
+ }
1301
+ /**
1302
+ * Convert images from DoclingDocument to ProcessedImage[]
1303
+ */
1304
+ async convertImages(doclingDoc, outputPath) {
1305
+ this.logger.info(
1306
+ `[ResourceConverter] Converting ${doclingDoc.pictures.length} images...`
1307
+ );
1308
+ const captionTexts = doclingDoc.pictures.map(
1309
+ (picture) => this.captionProcessingPipeline.extractCaptionText(picture.captions)
1310
+ );
1311
+ const images = doclingDoc.pictures.map(
1312
+ (picture, index) => ({
1313
+ id: this.idGenerator.generateImageId(),
1314
+ path: `${outputPath}/images/image_${index}.png`,
1315
+ pdfPageNo: picture.prov?.[0]?.page_no ?? 0
1316
+ })
1317
+ );
1318
+ const captionsByIndex = await this.captionProcessingPipeline.processResourceCaptions(
1319
+ captionTexts,
1320
+ "image"
1321
+ );
1322
+ images.forEach((image, i) => {
1323
+ if (captionsByIndex.has(i)) {
1324
+ image.caption = captionsByIndex.get(i);
1325
+ }
1326
+ });
1327
+ return images;
1328
+ }
1329
+ /**
1330
+ * Convert tables from DoclingDocument to ProcessedTable[]
1331
+ */
1332
+ async convertTables(doclingDoc) {
1333
+ this.logger.info(
1334
+ `[ResourceConverter] Converting ${doclingDoc.tables.length} tables...`
1335
+ );
1336
+ const captionTexts = doclingDoc.tables.map(
1337
+ (table) => this.captionProcessingPipeline.extractCaptionText(table.captions)
1338
+ );
1339
+ const tables = doclingDoc.tables.map((table) => {
1340
+ const grid = table.data.grid.map(
1341
+ (row) => row.map((cell) => ({
1342
+ text: cell.text,
1343
+ rowSpan: cell.row_span ?? 1,
1344
+ colSpan: cell.col_span ?? 1,
1345
+ isHeader: cell.column_header || cell.row_header || false
1346
+ }))
1347
+ );
1348
+ return {
1349
+ id: this.idGenerator.generateTableId(),
1350
+ pdfPageNo: table.prov?.[0]?.page_no ?? 0,
1351
+ numRows: grid.length,
1352
+ numCols: grid[0]?.length ?? 0,
1353
+ grid
1354
+ };
1355
+ });
1356
+ const captionsByIndex = await this.captionProcessingPipeline.processResourceCaptions(
1357
+ captionTexts,
1358
+ "table"
1359
+ );
1360
+ tables.forEach((table, i) => {
1361
+ if (captionsByIndex.has(i)) {
1362
+ table.caption = captionsByIndex.get(i);
1363
+ }
1364
+ });
1365
+ return tables;
1366
+ }
1367
+ /**
1368
+ * Convert footnotes from DoclingDocument text items
1369
+ */
1370
+ convertFootnotes(doclingDoc) {
1371
+ const footnoteItems = doclingDoc.texts.filter(
1372
+ (item) => item.label === "footnote"
1373
+ );
1374
+ this.logger.info(
1375
+ `[ResourceConverter] Converting ${footnoteItems.length} footnotes...`
1376
+ );
1377
+ const footnotes = footnoteItems.filter((item) => TextCleaner.isValidText(item.text)).map((item) => ({
1378
+ id: this.idGenerator.generateFootnoteId(),
1379
+ text: TextCleaner.normalize(item.text),
1380
+ pdfPageNo: item.prov?.[0]?.page_no ?? 1
1381
+ }));
1382
+ this.logger.info(
1383
+ `[ResourceConverter] Converted ${footnotes.length} valid footnotes`
1384
+ );
1385
+ return footnotes;
1386
+ }
1387
+ };
1388
+
1292
1389
  // src/extractors/toc-extract-error.ts
1293
1390
  var TocExtractError = class _TocExtractError extends Error {
1294
1391
  constructor(message, options) {
@@ -1670,43 +1767,33 @@ var TocFinder = class {
1670
1767
  */
1671
1768
  findByStructure(doc) {
1672
1769
  const candidates = [];
1673
- for (const group of doc.groups) {
1674
- const pageNo = this.getGroupFirstPage(group);
1675
- if (pageNo === void 0 || pageNo > this.maxSearchPages) {
1676
- continue;
1677
- }
1678
- if (this.isGroupTocLike(group, doc)) {
1679
- const score = this.calculateScore(group, pageNo);
1680
- candidates.push({
1681
- result: {
1682
- itemRefs: [group.self_ref],
1683
- startPage: pageNo,
1684
- endPage: pageNo
1685
- },
1686
- score
1687
- });
1688
- }
1689
- }
1690
- for (const table of doc.tables) {
1770
+ doc.groups.map((group) => ({ group, pageNo: this.getGroupFirstPage(group) })).filter(
1771
+ ({ group, pageNo }) => pageNo !== void 0 && pageNo <= this.maxSearchPages && this.isGroupTocLike(group, doc)
1772
+ ).forEach(({ group, pageNo }) => {
1773
+ candidates.push({
1774
+ result: {
1775
+ itemRefs: [group.self_ref],
1776
+ startPage: pageNo,
1777
+ endPage: pageNo
1778
+ },
1779
+ score: this.calculateScore(group, pageNo)
1780
+ });
1781
+ });
1782
+ doc.tables.filter((table) => {
1691
1783
  const pageNo = table.prov[0]?.page_no;
1692
- if (pageNo === void 0 || pageNo > this.maxSearchPages) {
1693
- continue;
1694
- }
1695
- if (this.isTableTocLike(table)) {
1696
- let score = this.calculateTableScore(table, pageNo);
1697
- if (this.isResourceIndexTable(table)) {
1698
- score -= 100;
1699
- }
1700
- candidates.push({
1701
- result: {
1702
- itemRefs: [table.self_ref],
1703
- startPage: pageNo,
1704
- endPage: pageNo
1705
- },
1706
- score
1707
- });
1708
- }
1709
- }
1784
+ return pageNo !== void 0 && pageNo <= this.maxSearchPages && this.isTableTocLike(table);
1785
+ }).forEach((table) => {
1786
+ const pageNo = table.prov[0].page_no;
1787
+ const score = this.calculateTableScore(table, pageNo) - (this.isResourceIndexTable(table) ? 100 : 0);
1788
+ candidates.push({
1789
+ result: {
1790
+ itemRefs: [table.self_ref],
1791
+ startPage: pageNo,
1792
+ endPage: pageNo
1793
+ },
1794
+ score
1795
+ });
1796
+ });
1710
1797
  if (candidates.length === 0) {
1711
1798
  return null;
1712
1799
  }
@@ -1778,17 +1865,10 @@ var TocFinder = class {
1778
1865
  if (group.name !== "list" && group.name !== "group") {
1779
1866
  return false;
1780
1867
  }
1781
- let pageNumberCount = 0;
1782
1868
  const children = this.refResolver.resolveMany(group.children);
1783
- for (const child of children) {
1784
- if (!child) continue;
1785
- if ("text" in child && "orig" in child) {
1786
- const textItem = child;
1787
- if (PAGE_NUMBER_PATTERN.test(textItem.text)) {
1788
- pageNumberCount++;
1789
- }
1790
- }
1791
- }
1869
+ const pageNumberCount = children.filter(
1870
+ (child) => child !== null && "text" in child && "orig" in child && PAGE_NUMBER_PATTERN.test(child.text)
1871
+ ).length;
1792
1872
  const total = children.filter((c) => c !== null).length;
1793
1873
  return pageNumberCount >= 3 || total > 0 && pageNumberCount / total > 0.5;
1794
1874
  }
@@ -1803,13 +1883,10 @@ var TocFinder = class {
1803
1883
  if (num_rows < 3 || num_cols < 2) {
1804
1884
  return false;
1805
1885
  }
1806
- let numberCount = 0;
1807
- for (let row = 1; row < grid.length; row++) {
1808
- const lastCell = grid[row]?.[num_cols - 1];
1809
- if (lastCell && /^\d+$/.test(lastCell.text.trim())) {
1810
- numberCount++;
1811
- }
1812
- }
1886
+ const numberCount = grid.slice(1).filter((row) => {
1887
+ const lastCell = row?.[num_cols - 1];
1888
+ return lastCell && /^\d+$/.test(lastCell.text.trim());
1889
+ }).length;
1813
1890
  return numberCount > 0 && numberCount / (num_rows - 1) > 0.5;
1814
1891
  }
1815
1892
  /**
@@ -1821,12 +1898,9 @@ var TocFinder = class {
1821
1898
  (c) => c.start_col_offset_idx === 0
1822
1899
  );
1823
1900
  if (firstColCells.length === 0) return false;
1824
- let resourceCount = 0;
1825
- for (const cell of firstColCells) {
1826
- if (RESOURCE_INDEX_PATTERNS.some((p) => p.test(cell.text.trim()))) {
1827
- resourceCount++;
1828
- }
1829
- }
1901
+ const resourceCount = firstColCells.filter(
1902
+ (cell) => RESOURCE_INDEX_PATTERNS.some((p) => p.test(cell.text.trim()))
1903
+ ).length;
1830
1904
  return resourceCount / firstColCells.length > 0.5;
1831
1905
  }
1832
1906
  /**
@@ -1843,9 +1917,7 @@ var TocFinder = class {
1843
1917
  break;
1844
1918
  }
1845
1919
  const newItems = continuationItems.filter((ref) => !seenRefs.has(ref));
1846
- for (const ref of newItems) {
1847
- seenRefs.add(ref);
1848
- }
1920
+ newItems.forEach((ref) => seenRefs.add(ref));
1849
1921
  itemRefs.unshift(...newItems);
1850
1922
  startPage = pageNo;
1851
1923
  this.logger.info(`[TocFinder] Expanded TOC backward to page ${pageNo}`);
@@ -1856,9 +1928,7 @@ var TocFinder = class {
1856
1928
  break;
1857
1929
  }
1858
1930
  const newItems = continuationItems.filter((ref) => !seenRefs.has(ref));
1859
- for (const ref of newItems) {
1860
- seenRefs.add(ref);
1861
- }
1931
+ newItems.forEach((ref) => seenRefs.add(ref));
1862
1932
  itemRefs.push(...newItems);
1863
1933
  endPage = pageNo;
1864
1934
  this.logger.info(`[TocFinder] Expanded TOC forward to page ${pageNo}`);
@@ -1874,37 +1944,27 @@ var TocFinder = class {
1874
1944
  */
1875
1945
  findContinuationOnPage(doc, pageNo) {
1876
1946
  const refs = [];
1877
- for (const text of doc.texts) {
1878
- if (text.prov[0]?.page_no !== pageNo) {
1879
- continue;
1880
- }
1881
- if (this.hasContinuationMarker(text.text)) {
1882
- const parentRef = text.parent?.$ref;
1883
- if (parentRef) {
1884
- const group = this.refResolver.resolveGroup(parentRef);
1885
- if (group) {
1886
- refs.push(group.self_ref);
1887
- }
1947
+ doc.texts.filter(
1948
+ (text) => text.prov[0]?.page_no === pageNo && this.hasContinuationMarker(text.text)
1949
+ ).forEach((text) => {
1950
+ const parentRef = text.parent?.$ref;
1951
+ if (parentRef) {
1952
+ const group = this.refResolver.resolveGroup(parentRef);
1953
+ if (group) {
1954
+ refs.push(group.self_ref);
1888
1955
  }
1889
1956
  }
1890
- }
1891
- for (const group of doc.groups) {
1892
- const groupPage = this.getGroupFirstPage(group);
1893
- if (groupPage !== pageNo) {
1894
- continue;
1895
- }
1896
- if (this.isGroupTocLike(group, doc) && !refs.includes(group.self_ref)) {
1897
- refs.push(group.self_ref);
1898
- }
1899
- }
1900
- for (const table of doc.tables) {
1901
- if (table.prov[0]?.page_no !== pageNo) {
1902
- continue;
1903
- }
1904
- if (this.isTableTocLike(table) && !this.isResourceIndexTable(table) && !refs.includes(table.self_ref)) {
1905
- refs.push(table.self_ref);
1906
- }
1907
- }
1957
+ });
1958
+ doc.groups.filter(
1959
+ (group) => this.getGroupFirstPage(group) === pageNo && this.isGroupTocLike(group, doc) && !refs.includes(group.self_ref)
1960
+ ).forEach((group) => {
1961
+ refs.push(group.self_ref);
1962
+ });
1963
+ doc.tables.filter(
1964
+ (table) => table.prov[0]?.page_no === pageNo && this.isTableTocLike(table) && !this.isResourceIndexTable(table) && !refs.includes(table.self_ref)
1965
+ ).forEach((table) => {
1966
+ refs.push(table.self_ref);
1967
+ });
1908
1968
  return refs;
1909
1969
  }
1910
1970
  /**
@@ -1929,16 +1989,10 @@ var TocFinder = class {
1929
1989
  * Get first page number of a group by checking its children
1930
1990
  */
1931
1991
  getGroupFirstPage(group) {
1932
- for (const childRef of group.children) {
1933
- const child = this.refResolver.resolve(childRef.$ref);
1934
- if (child && "prov" in child) {
1935
- const prov = child.prov;
1936
- if (prov && prov[0]?.page_no !== void 0) {
1937
- return prov[0].page_no;
1938
- }
1939
- }
1940
- }
1941
- return void 0;
1992
+ const found = group.children.map((childRef) => this.refResolver.resolve(childRef.$ref)).find(
1993
+ (child) => child !== null && "prov" in child && child.prov?.[0]?.page_no !== void 0
1994
+ );
1995
+ return found ? found.prov[0]?.page_no : void 0;
1942
1996
  }
1943
1997
  /**
1944
1998
  * Calculate score for a group candidate
@@ -1949,14 +2003,9 @@ var TocFinder = class {
1949
2003
  score += (this.maxSearchPages - pageNo + 1) * 10;
1950
2004
  score += group.children.length * 2;
1951
2005
  const children = this.refResolver.resolveMany(group.children);
1952
- for (const child of children) {
1953
- if (child && "text" in child) {
1954
- const textItem = child;
1955
- if (PAGE_NUMBER_PATTERN.test(textItem.text)) {
1956
- score += 5;
1957
- }
1958
- }
1959
- }
2006
+ score += children.filter(
2007
+ (child) => child !== null && "text" in child && PAGE_NUMBER_PATTERN.test(child.text)
2008
+ ).length * 5;
1960
2009
  return score;
1961
2010
  }
1962
2011
  /**
@@ -3034,9 +3083,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3034
3083
  Object.assign(pageRangeMap, groupResult.pageRangeMap);
3035
3084
  usageList.push(...groupResult.usage);
3036
3085
  }
3037
- for (const usage of usageList) {
3038
- this.trackUsage(usage);
3039
- }
3086
+ usageList.forEach((usage) => this.trackUsage(usage));
3040
3087
  this.postProcess(pageRangeMap);
3041
3088
  this.log(
3042
3089
  "info",
@@ -3104,9 +3151,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3104
3151
  this.SAMPLE_SIZE,
3105
3152
  sampledPages
3106
3153
  );
3107
- for (const p of samplePageNos) {
3108
- sampledPages.add(p);
3109
- }
3154
+ samplePageNos.forEach((p) => sampledPages.add(p));
3110
3155
  this.log(
3111
3156
  "info",
3112
3157
  `Attempt ${attempt + 1}/${this.MAX_PATTERN_RETRIES + 1}: sampling pages ${samplePageNos.join(", ")}`
@@ -3166,10 +3211,9 @@ var PageRangeParser = class extends VisionLLMComponent {
3166
3211
  "warn",
3167
3212
  `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`
3168
3213
  );
3169
- const fallbackMap = {};
3170
- for (const pageNo of pageNos) {
3171
- fallbackMap[pageNo] = { startPageNo: 0, endPageNo: 0 };
3172
- }
3214
+ const fallbackMap = Object.fromEntries(
3215
+ pageNos.map((pageNo) => [pageNo, { startPageNo: 0, endPageNo: 0 }])
3216
+ );
3173
3217
  return { pageRangeMap: fallbackMap, usage: usageList };
3174
3218
  }
3175
3219
  /**
@@ -3191,7 +3235,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3191
3235
  async extractMultiplePages(pages, pageNos, model) {
3192
3236
  this.log("info", `Extracting ${pageNos.length} pages in single LLM call`);
3193
3237
  const imageContents = [];
3194
- for (const pageNo of pageNos) {
3238
+ const imageContentsFromPages = pageNos.map((pageNo) => {
3195
3239
  const page = pages[pageNo - 1];
3196
3240
  const imagePath = path3.resolve(
3197
3241
  this.outputPath,
@@ -3199,12 +3243,13 @@ var PageRangeParser = class extends VisionLLMComponent {
3199
3243
  );
3200
3244
  const imageData = new Uint8Array(fs3.readFileSync(imagePath));
3201
3245
  const mimeType = page.image.mimetype || "image/png";
3202
- imageContents.push({
3246
+ return {
3203
3247
  type: "image",
3204
3248
  image: imageData,
3205
3249
  mediaType: mimeType
3206
- });
3207
- }
3250
+ };
3251
+ });
3252
+ imageContents.push(...imageContentsFromPages);
3208
3253
  const schema = import_zod4.z.object({
3209
3254
  pages: import_zod4.z.array(
3210
3255
  import_zod4.z.object({
@@ -3303,7 +3348,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3303
3348
  */
3304
3349
  applyPattern(pageNos, pattern) {
3305
3350
  const result = {};
3306
- for (const pdfPageNo of pageNos) {
3351
+ pageNos.forEach((pdfPageNo) => {
3307
3352
  switch (pattern.pattern) {
3308
3353
  case "simple_increment" /* SIMPLE_INCREMENT */:
3309
3354
  case "offset" /* OFFSET */: {
@@ -3325,25 +3370,22 @@ var PageRangeParser = class extends VisionLLMComponent {
3325
3370
  default:
3326
3371
  result[pdfPageNo] = { startPageNo: 0, endPageNo: 0 };
3327
3372
  }
3328
- }
3373
+ });
3329
3374
  return result;
3330
3375
  }
3331
3376
  /**
3332
3377
  * Convert sample results to page range map (for small groups)
3333
3378
  */
3334
3379
  samplesToMap(samples) {
3335
- const result = {};
3336
- for (const sample of samples) {
3337
- if (sample.startPageNo !== null) {
3338
- result[sample.pdfPageNo] = {
3380
+ return Object.fromEntries(
3381
+ samples.map((sample) => [
3382
+ sample.pdfPageNo,
3383
+ sample.startPageNo !== null ? {
3339
3384
  startPageNo: sample.startPageNo,
3340
3385
  endPageNo: sample.endPageNo ?? sample.startPageNo
3341
- };
3342
- } else {
3343
- result[sample.pdfPageNo] = { startPageNo: 0, endPageNo: 0 };
3344
- }
3345
- }
3346
- return result;
3386
+ } : { startPageNo: 0, endPageNo: 0 }
3387
+ ])
3388
+ );
3347
3389
  }
3348
3390
  /**
3349
3391
  * Post-process the page range map
@@ -3630,42 +3672,357 @@ Remember: Look for SMALL numbers in MARGINS only. Ignore figure/table/photo numb
3630
3672
  }
3631
3673
  };
3632
3674
 
3633
- // src/validators/base-validator.ts
3634
- var BaseValidator = class extends TextLLMComponent {
3635
- /**
3636
- * Validator name for logging (kept for backwards compatibility)
3637
- */
3638
- validatorName;
3639
- /**
3640
- * Constructor for BaseValidator
3641
- *
3642
- * @param logger - Logger instance
3643
- * @param model - Language model to use for validation
3644
- * @param validatorName - Name of the validator for logging (e.g., "TocContentValidator")
3645
- * @param options - Optional configuration (maxRetries, temperature)
3646
- * @param fallbackModel - Optional fallback model for retry on failure
3647
- * @param aggregator - Optional token usage aggregator for tracking LLM calls
3648
- */
3649
- constructor(logger, model, validatorName, options, fallbackModel, aggregator) {
3650
- super(logger, model, validatorName, options, fallbackModel, aggregator);
3651
- this.validatorName = validatorName;
3675
+ // src/pipelines/caption-processing-pipeline.ts
3676
+ var CaptionProcessingPipeline = class {
3677
+ logger;
3678
+ captionParser;
3679
+ captionValidator;
3680
+ refResolver;
3681
+ fallbackModel;
3682
+ enableFallbackRetry;
3683
+ maxRetries;
3684
+ captionParserBatchSize;
3685
+ captionValidatorBatchSize;
3686
+ usageAggregator;
3687
+ abortSignal;
3688
+ constructor(deps) {
3689
+ this.logger = deps.logger;
3690
+ this.captionParser = deps.captionParser;
3691
+ this.captionValidator = deps.captionValidator;
3692
+ this.refResolver = deps.refResolver;
3693
+ this.fallbackModel = deps.fallbackModel;
3694
+ this.enableFallbackRetry = deps.enableFallbackRetry;
3695
+ this.maxRetries = deps.maxRetries;
3696
+ this.captionParserBatchSize = deps.captionParserBatchSize;
3697
+ this.captionValidatorBatchSize = deps.captionValidatorBatchSize;
3698
+ this.usageAggregator = deps.usageAggregator;
3699
+ this.abortSignal = deps.abortSignal;
3652
3700
  }
3653
3701
  /**
3654
- * Call LLM with LLMCaller
3702
+ * Process resource captions (for images and tables)
3655
3703
  *
3656
- * This method provides backwards compatibility with existing validators.
3657
- * It wraps the parent callTextLLM method but allows passing a custom aggregator.
3704
+ * Common caption processing pipeline:
3705
+ * 1. Parse captions in batch
3706
+ * 2. Validate parsed captions
3707
+ * 3. Reparse failed captions with fallback model
3658
3708
  *
3659
- * @param schema - Zod schema for response validation
3660
- * @param systemPrompt - System prompt
3661
- * @param userPrompt - User prompt
3662
- * @param phase - Phase name for tracking (e.g., 'validation', 'batch-validation')
3663
- * @param aggregator - Optional token usage aggregator for tracking this call
3664
- * @returns Parsed and validated LLM response with usage information
3709
+ * @param captionTexts - Array of caption texts to process
3710
+ * @param resourceType - Type of resource for logging (e.g., 'image', 'table')
3711
+ * @returns Parsed captions with index mapping
3665
3712
  */
3666
- async callLLM(schema, systemPrompt, userPrompt, phase, aggregator) {
3667
- const result = await LLMCaller.call({
3668
- schema,
3713
+ async processResourceCaptions(captionTexts, resourceType) {
3714
+ const captionsByIndex = /* @__PURE__ */ new Map();
3715
+ const validCaptionData = [];
3716
+ captionTexts.forEach((text, i) => {
3717
+ if (text !== void 0) {
3718
+ validCaptionData.push({
3719
+ resourceIndex: i,
3720
+ filteredIndex: validCaptionData.length,
3721
+ text
3722
+ });
3723
+ }
3724
+ });
3725
+ const validCaptionTexts = validCaptionData.map((item) => item.text);
3726
+ const parsedCaptions = validCaptionTexts.length > 0 ? await this.captionParser.parseBatch(
3727
+ validCaptionTexts,
3728
+ this.captionParserBatchSize
3729
+ ) : [];
3730
+ let finalValidCaptionData = validCaptionData;
3731
+ let finalParsedCaptions = parsedCaptions;
3732
+ if (parsedCaptions.length !== validCaptionData.length) {
3733
+ this.logger.warn(
3734
+ `[CaptionProcessingPipeline] Caption parsing length mismatch for ${resourceType}: expected ${validCaptionData.length}, got ${parsedCaptions.length}. Attempting recovery by matching fullText...`
3735
+ );
3736
+ const parsedMap = new Map(
3737
+ parsedCaptions.map((parsed) => [parsed.fullText, parsed])
3738
+ );
3739
+ const recoveredData = validCaptionData.filter((item) => {
3740
+ if (parsedMap.has(item.text)) {
3741
+ return true;
3742
+ }
3743
+ this.logger.warn(
3744
+ `[CaptionProcessingPipeline] Skipping ${resourceType} caption at index ${item.resourceIndex}: "${item.text}" (not found in parsed results)`
3745
+ );
3746
+ return false;
3747
+ });
3748
+ const recoveredCaptions = recoveredData.map((item) => parsedMap.get(item.text)).filter((caption) => caption !== void 0);
3749
+ if (recoveredCaptions.length !== recoveredData.length) {
3750
+ throw new Error(
3751
+ `[CaptionProcessingPipeline] Failed to recover from length mismatch: recovered ${recoveredCaptions.length} captions for ${recoveredData.length} valid items`
3752
+ );
3753
+ }
3754
+ finalValidCaptionData = recoveredData;
3755
+ finalParsedCaptions = recoveredCaptions;
3756
+ this.logger.info(
3757
+ `[CaptionProcessingPipeline] Successfully recovered ${finalParsedCaptions.length} ${resourceType} captions after length mismatch`
3758
+ );
3759
+ }
3760
+ finalParsedCaptions.forEach((caption, i) => {
3761
+ const resourceIndex = finalValidCaptionData[i].resourceIndex;
3762
+ captionsByIndex.set(resourceIndex, caption);
3763
+ });
3764
+ if (finalParsedCaptions.length > 0) {
3765
+ const finalValidCaptionTexts = finalValidCaptionData.map(
3766
+ (item) => item.text
3767
+ );
3768
+ const validationResults = await this.captionValidator.validateBatch(
3769
+ finalParsedCaptions,
3770
+ finalValidCaptionTexts,
3771
+ this.captionValidatorBatchSize
3772
+ );
3773
+ const failedIndices = validationResults.map((isValid, index) => isValid ? -1 : index).filter((index) => index !== -1);
3774
+ if (failedIndices.length > 0) {
3775
+ failedIndices.forEach((filteredIndex) => {
3776
+ const captionData = finalValidCaptionData[filteredIndex];
3777
+ const originalText = captionData.text;
3778
+ const parsedNum = finalParsedCaptions[filteredIndex].num;
3779
+ const resourceIndex = captionData.resourceIndex;
3780
+ this.logger.warn(
3781
+ `[CaptionProcessingPipeline] Invalid ${resourceType} caption [${resourceIndex}]: "${originalText}" | parsed num="${parsedNum}"`
3782
+ );
3783
+ });
3784
+ if (this.enableFallbackRetry) {
3785
+ this.logger.info(
3786
+ `[CaptionProcessingPipeline] Reparsing ${failedIndices.length} failed ${resourceType} captions with fallback model...`
3787
+ );
3788
+ const failedCaptionTexts = failedIndices.map(
3789
+ (filteredIndex) => finalValidCaptionData[filteredIndex].text
3790
+ );
3791
+ const fallbackCaptionParser = new CaptionParser(
3792
+ this.logger,
3793
+ this.fallbackModel,
3794
+ {
3795
+ maxRetries: this.maxRetries,
3796
+ componentName: "CaptionParser-fallback",
3797
+ abortSignal: this.abortSignal
3798
+ },
3799
+ void 0,
3800
+ // no fallback for the fallback
3801
+ this.usageAggregator
3802
+ );
3803
+ const reparsedCaptions = await fallbackCaptionParser.parseBatch(
3804
+ failedCaptionTexts,
3805
+ 0
3806
+ // sequential processing
3807
+ );
3808
+ failedIndices.forEach((filteredIndex, i) => {
3809
+ const resourceIndex = finalValidCaptionData[filteredIndex].resourceIndex;
3810
+ captionsByIndex.set(resourceIndex, reparsedCaptions[i]);
3811
+ });
3812
+ this.logger.info(
3813
+ `[CaptionProcessingPipeline] Reparsed ${reparsedCaptions.length} ${resourceType} captions`
3814
+ );
3815
+ } else {
3816
+ this.logger.warn(
3817
+ `[CaptionProcessingPipeline] ${failedIndices.length} ${resourceType} captions failed validation (kept as-is, fallback retry disabled)`
3818
+ );
3819
+ }
3820
+ }
3821
+ }
3822
+ return captionsByIndex;
3823
+ }
3824
+ /**
3825
+ * Extract caption text from resource
3826
+ *
3827
+ * Handles both string references and $ref resolution
3828
+ */
3829
+ extractCaptionText(captions) {
3830
+ if (!captions?.[0]) {
3831
+ return void 0;
3832
+ }
3833
+ const captionRef = captions[0];
3834
+ if (typeof captionRef === "string") {
3835
+ return captionRef;
3836
+ }
3837
+ if (this.refResolver && "$ref" in captionRef) {
3838
+ const resolved = this.refResolver.resolveText(captionRef.$ref);
3839
+ return resolved?.text;
3840
+ }
3841
+ return void 0;
3842
+ }
3843
+ };
3844
+
3845
+ // src/pipelines/toc-extraction-pipeline.ts
3846
+ var TocExtractionPipeline = class {
3847
+ logger;
3848
+ tocFinder;
3849
+ tocExtractor;
3850
+ tocContentValidator;
3851
+ visionTocExtractor;
3852
+ refResolver;
3853
+ usageAggregator;
3854
+ constructor(deps) {
3855
+ this.logger = deps.logger;
3856
+ this.tocFinder = deps.tocFinder;
3857
+ this.tocExtractor = deps.tocExtractor;
3858
+ this.tocContentValidator = deps.tocContentValidator;
3859
+ this.visionTocExtractor = deps.visionTocExtractor;
3860
+ this.refResolver = deps.refResolver;
3861
+ this.usageAggregator = deps.usageAggregator;
3862
+ }
3863
+ /**
3864
+ * Execute the TOC extraction pipeline
3865
+ *
3866
+ * @param doclingDoc - Document containing texts, pages, etc.
3867
+ * @param _filteredTexts - Pre-filtered texts (reserved for future use)
3868
+ * @returns Extracted TOC entries
3869
+ * @throws {TocNotFoundError} When TOC cannot be found or extracted
3870
+ */
3871
+ async extract(doclingDoc, _filteredTexts) {
3872
+ this.logger.info("[TocExtractionPipeline] Extracting TOC...");
3873
+ let markdown = null;
3874
+ try {
3875
+ const tocArea = this.tocFinder.find(doclingDoc);
3876
+ this.logger.info(
3877
+ `[TocExtractionPipeline] Found TOC area: pages ${tocArea.startPage}-${tocArea.endPage}`
3878
+ );
3879
+ markdown = MarkdownConverter.convert(tocArea.itemRefs, this.refResolver);
3880
+ this.logger.info(
3881
+ `[TocExtractionPipeline] Converted TOC to Markdown (${markdown.length} chars)`
3882
+ );
3883
+ const validation = await this.tocContentValidator.validate(markdown);
3884
+ if (!this.tocContentValidator.isValid(validation)) {
3885
+ this.logger.warn(
3886
+ `[TocExtractionPipeline] TOC validation failed: ${validation.reason}`
3887
+ );
3888
+ markdown = null;
3889
+ } else {
3890
+ const validMarkdown = this.tocContentValidator.getValidMarkdown(validation);
3891
+ if (validMarkdown) {
3892
+ if (validation.contentType === "mixed") {
3893
+ this.logger.info(
3894
+ `[TocExtractionPipeline] Mixed TOC detected, using extracted main TOC (${validMarkdown.length} chars)`
3895
+ );
3896
+ }
3897
+ markdown = validMarkdown;
3898
+ this.logger.info(
3899
+ `[TocExtractionPipeline] TOC validation passed (confidence: ${validation.confidence})`
3900
+ );
3901
+ } else {
3902
+ markdown = null;
3903
+ }
3904
+ }
3905
+ } catch (error) {
3906
+ if (error instanceof TocNotFoundError) {
3907
+ this.logger.info(
3908
+ "[TocExtractionPipeline] Rule-based TOC not found, will try vision fallback"
3909
+ );
3910
+ } else {
3911
+ throw error;
3912
+ }
3913
+ }
3914
+ let fromVision = false;
3915
+ const totalPages = Object.keys(doclingDoc.pages).length;
3916
+ if (!markdown) {
3917
+ fromVision = true;
3918
+ this.logger.info("[TocExtractionPipeline] Using vision fallback for TOC");
3919
+ markdown = await this.visionTocExtractor.extract(totalPages);
3920
+ if (!markdown) {
3921
+ const reason = "Both rule-based search and vision fallback failed to locate TOC";
3922
+ this.logger.error(
3923
+ `[TocExtractionPipeline] TOC extraction failed: ${reason}`
3924
+ );
3925
+ throw new TocNotFoundError(
3926
+ `Table of contents not found in the document. ${reason}.`
3927
+ );
3928
+ }
3929
+ this.logger.info(
3930
+ `[TocExtractionPipeline] Vision extracted TOC markdown (${markdown.length} chars)`
3931
+ );
3932
+ }
3933
+ const maxTocPageNo = extractMaxPageNumber(markdown);
3934
+ const effectiveTotalPages = maxTocPageNo > totalPages ? void 0 : totalPages;
3935
+ let tocResult;
3936
+ try {
3937
+ tocResult = await this.tocExtractor.extract(markdown, {
3938
+ totalPages: effectiveTotalPages
3939
+ });
3940
+ } catch (error) {
3941
+ if (error instanceof TocValidationError) {
3942
+ this.logger.warn(
3943
+ `[TocExtractionPipeline] TOC extraction validation failed: ${error.message}`
3944
+ );
3945
+ tocResult = { entries: [], usages: [] };
3946
+ } else {
3947
+ throw error;
3948
+ }
3949
+ }
3950
+ tocResult.usages.forEach((usage) => this.usageAggregator.track(usage));
3951
+ if (tocResult.entries.length === 0 && !fromVision) {
3952
+ this.logger.warn(
3953
+ "[TocExtractionPipeline] Text-based TOC extraction yielded 0 entries, retrying with vision"
3954
+ );
3955
+ const visionMarkdown = await this.visionTocExtractor.extract(totalPages);
3956
+ if (visionMarkdown) {
3957
+ this.logger.info(
3958
+ `[TocExtractionPipeline] Vision extracted TOC markdown (${visionMarkdown.length} chars)`
3959
+ );
3960
+ const visionMaxPageNo = extractMaxPageNumber(visionMarkdown);
3961
+ const visionEffectivePages = visionMaxPageNo > totalPages ? void 0 : totalPages;
3962
+ try {
3963
+ const visionResult = await this.tocExtractor.extract(visionMarkdown, {
3964
+ totalPages: visionEffectivePages
3965
+ });
3966
+ visionResult.usages.forEach(
3967
+ (usage) => this.usageAggregator.track(usage)
3968
+ );
3969
+ if (visionResult.entries.length > 0) {
3970
+ tocResult = visionResult;
3971
+ }
3972
+ } catch {
3973
+ }
3974
+ }
3975
+ }
3976
+ if (tocResult.entries.length === 0) {
3977
+ const reason = "TOC area was detected but LLM could not extract any structured entries";
3978
+ this.logger.error(
3979
+ `[TocExtractionPipeline] TOC extraction failed: ${reason}`
3980
+ );
3981
+ throw new TocNotFoundError(`${reason}.`);
3982
+ }
3983
+ this.logger.info(
3984
+ `[TocExtractionPipeline] Extracted ${tocResult.entries.length} top-level TOC entries`
3985
+ );
3986
+ return tocResult.entries;
3987
+ }
3988
+ };
3989
+
3990
+ // src/validators/base-validator.ts
3991
+ var BaseValidator = class extends TextLLMComponent {
3992
+ /**
3993
+ * Validator name for logging (kept for backwards compatibility)
3994
+ */
3995
+ validatorName;
3996
+ /**
3997
+ * Constructor for BaseValidator
3998
+ *
3999
+ * @param logger - Logger instance
4000
+ * @param model - Language model to use for validation
4001
+ * @param validatorName - Name of the validator for logging (e.g., "TocContentValidator")
4002
+ * @param options - Optional configuration (maxRetries, temperature)
4003
+ * @param fallbackModel - Optional fallback model for retry on failure
4004
+ * @param aggregator - Optional token usage aggregator for tracking LLM calls
4005
+ */
4006
+ constructor(logger, model, validatorName, options, fallbackModel, aggregator) {
4007
+ super(logger, model, validatorName, options, fallbackModel, aggregator);
4008
+ this.validatorName = validatorName;
4009
+ }
4010
+ /**
4011
+ * Call LLM with LLMCaller
4012
+ *
4013
+ * This method provides backwards compatibility with existing validators.
4014
+ * It wraps the parent callTextLLM method but allows passing a custom aggregator.
4015
+ *
4016
+ * @param schema - Zod schema for response validation
4017
+ * @param systemPrompt - System prompt
4018
+ * @param userPrompt - User prompt
4019
+ * @param phase - Phase name for tracking (e.g., 'validation', 'batch-validation')
4020
+ * @param aggregator - Optional token usage aggregator for tracking this call
4021
+ * @returns Parsed and validated LLM response with usage information
4022
+ */
4023
+ async callLLM(schema, systemPrompt, userPrompt, phase, aggregator) {
4024
+ const result = await LLMCaller.call({
4025
+ schema,
3669
4026
  systemPrompt,
3670
4027
  userPrompt,
3671
4028
  primaryModel: this.model,
@@ -4078,7 +4435,9 @@ var DocumentProcessor = class {
4078
4435
  visionTocExtractor;
4079
4436
  captionParser;
4080
4437
  chapterConverter;
4081
- textCleaner = TextCleaner;
4438
+ resourceConverter;
4439
+ tocExtractionPipeline;
4440
+ captionProcessingPipeline;
4082
4441
  usageAggregator = new LLMTokenUsageAggregator();
4083
4442
  constructor(options) {
4084
4443
  this.logger = options.logger;
@@ -4161,16 +4520,16 @@ var DocumentProcessor = class {
4161
4520
  this.emitTokenUsage();
4162
4521
  this.checkAborted();
4163
4522
  const startTimeToc = Date.now();
4164
- const tocEntries = await this.extractTableOfContents(doclingDoc, filtered);
4523
+ const tocEntries = await this.tocExtractionPipeline.extract(
4524
+ doclingDoc,
4525
+ filtered
4526
+ );
4165
4527
  const tocTime = Date.now() - startTimeToc;
4166
4528
  this.logger.info(`[DocumentProcessor] TOC extraction took ${tocTime}ms`);
4167
4529
  this.emitTokenUsage();
4168
4530
  this.checkAborted();
4169
4531
  const startTimeResources = Date.now();
4170
- const { images, tables, footnotes } = await this.convertResources(
4171
- doclingDoc,
4172
- outputPath
4173
- );
4532
+ const { images, tables, footnotes } = await this.resourceConverter.convertAll(doclingDoc, outputPath);
4174
4533
  const resourcesTime = Date.now() - startTimeResources;
4175
4534
  this.logger.info(
4176
4535
  `[DocumentProcessor] Resource conversion took ${resourcesTime}ms`
@@ -4275,7 +4634,37 @@ var DocumentProcessor = class {
4275
4634
  );
4276
4635
  this.logger.info("[DocumentProcessor] - ChapterConverter");
4277
4636
  this.chapterConverter = new ChapterConverter(this.logger, this.idGenerator);
4278
- this.logger.info("[DocumentProcessor] All processors initialized");
4637
+ this.logger.info("[DocumentProcessor] - TocExtractionPipeline");
4638
+ this.tocExtractionPipeline = new TocExtractionPipeline({
4639
+ logger: this.logger,
4640
+ tocFinder: this.tocFinder,
4641
+ tocExtractor: this.tocExtractor,
4642
+ tocContentValidator: this.tocContentValidator,
4643
+ visionTocExtractor: this.visionTocExtractor,
4644
+ refResolver: this.refResolver,
4645
+ usageAggregator: this.usageAggregator
4646
+ });
4647
+ this.logger.info("[DocumentProcessor] - CaptionProcessingPipeline");
4648
+ this.captionProcessingPipeline = new CaptionProcessingPipeline({
4649
+ logger: this.logger,
4650
+ captionParser: this.captionParser,
4651
+ captionValidator: this.captionValidator,
4652
+ refResolver: this.refResolver,
4653
+ fallbackModel: this.fallbackModel,
4654
+ enableFallbackRetry: this.enableFallbackRetry,
4655
+ maxRetries: this.maxRetries,
4656
+ captionParserBatchSize: this.captionParserBatchSize,
4657
+ captionValidatorBatchSize: this.captionValidatorBatchSize,
4658
+ usageAggregator: this.usageAggregator,
4659
+ abortSignal: this.abortSignal
4660
+ });
4661
+ this.logger.info("[DocumentProcessor] - ResourceConverter");
4662
+ this.resourceConverter = new ResourceConverter(
4663
+ this.logger,
4664
+ this.idGenerator,
4665
+ this.captionProcessingPipeline
4666
+ );
4667
+ this.logger.info("[DocumentProcessor] All processors initialized");
4279
4668
  }
4280
4669
  /**
4281
4670
  * Normalize and filter texts using TextCleaner
@@ -4286,7 +4675,7 @@ var DocumentProcessor = class {
4286
4675
  normalizeAndFilterTexts(doclingDoc) {
4287
4676
  this.logger.info("[DocumentProcessor] Normalizing and filtering texts...");
4288
4677
  const texts = doclingDoc.texts.map((text) => text.text);
4289
- const filtered = this.textCleaner.normalizeAndFilterBatch(
4678
+ const filtered = TextCleaner.normalizeAndFilterBatch(
4290
4679
  texts,
4291
4680
  this.textCleanerBatchSize
4292
4681
  );
@@ -4310,58 +4699,6 @@ var DocumentProcessor = class {
4310
4699
  );
4311
4700
  return pageRangeMap;
4312
4701
  }
4313
- /**
4314
- * Convert images, tables, and footnotes
4315
- *
4316
- * Runs conversions:
4317
- * - Images conversion (with caption extraction)
4318
- * - Tables conversion (with caption extraction, excluding TOC tables)
4319
- * - Footnotes conversion (synchronous, from text items with label='footnote')
4320
- */
4321
- async convertResources(doclingDoc, outputPath) {
4322
- this.logger.info(
4323
- "[DocumentProcessor] Converting images, tables, and footnotes..."
4324
- );
4325
- const [images, tables] = await Promise.all([
4326
- this.convertImages(doclingDoc, outputPath),
4327
- this.convertTables(doclingDoc)
4328
- ]);
4329
- const footnotes = this.convertFootnotes(doclingDoc);
4330
- this.logger.info(
4331
- `[DocumentProcessor] Converted ${images.length} images, ${tables.length} tables, and ${footnotes.length} footnotes`
4332
- );
4333
- return { images, tables, footnotes };
4334
- }
4335
- /**
4336
- * Convert footnotes
4337
- *
4338
- * Extracts footnotes from DoclingDocument text items with label='footnote'
4339
- */
4340
- convertFootnotes(doclingDoc) {
4341
- const footnoteItems = doclingDoc.texts.filter(
4342
- (item) => item.label === "footnote"
4343
- );
4344
- this.logger.info(
4345
- `[DocumentProcessor] Converting ${footnoteItems.length} footnotes...`
4346
- );
4347
- const footnotes = [];
4348
- for (const item of footnoteItems) {
4349
- if (!this.textCleaner.isValidText(item.text)) {
4350
- continue;
4351
- }
4352
- const pdfPageNo = item.prov?.[0]?.page_no ?? 1;
4353
- const footnoteId = this.idGenerator.generateFootnoteId();
4354
- footnotes.push({
4355
- id: footnoteId,
4356
- text: this.textCleaner.normalize(item.text),
4357
- pdfPageNo
4358
- });
4359
- }
4360
- this.logger.info(
4361
- `[DocumentProcessor] Converted ${footnotes.length} valid footnotes`
4362
- );
4363
- return footnotes;
4364
- }
4365
4702
  /**
4366
4703
  * Assemble the final ProcessedDocument
4367
4704
  *
@@ -4382,386 +4719,6 @@ var DocumentProcessor = class {
4382
4719
  );
4383
4720
  return processedDoc;
4384
4721
  }
4385
- /**
4386
- * Extract table of contents (TOC)
4387
- *
4388
- * Uses rule-based extraction with LLM validation and vision fallback:
4389
- * 1. TocFinder - find TOC area in document (rule-based)
4390
- * 2. MarkdownConverter - convert TOC items to Markdown
4391
- * 3. TocContentValidator - validate if content is actually a TOC (LLM)
4392
- * 4. If invalid: VisionTocExtractor - extract from page images (vision LLM fallback)
4393
- * 5. TocExtractor - LLM-based structured extraction
4394
- */
4395
- async extractTableOfContents(doclingDoc, _filteredTexts) {
4396
- this.logger.info("[DocumentProcessor] Extracting TOC...");
4397
- let markdown = null;
4398
- try {
4399
- const tocArea = this.tocFinder.find(doclingDoc);
4400
- this.logger.info(
4401
- `[DocumentProcessor] Found TOC area: pages ${tocArea.startPage}-${tocArea.endPage}`
4402
- );
4403
- markdown = MarkdownConverter.convert(tocArea.itemRefs, this.refResolver);
4404
- this.logger.info(
4405
- `[DocumentProcessor] Converted TOC to Markdown (${markdown.length} chars)`
4406
- );
4407
- const validation = await this.tocContentValidator.validate(markdown);
4408
- if (!this.tocContentValidator.isValid(validation)) {
4409
- this.logger.warn(
4410
- `[DocumentProcessor] TOC validation failed: ${validation.reason}`
4411
- );
4412
- markdown = null;
4413
- } else {
4414
- const validMarkdown = this.tocContentValidator.getValidMarkdown(validation);
4415
- if (validMarkdown) {
4416
- if (validation.contentType === "mixed") {
4417
- this.logger.info(
4418
- `[DocumentProcessor] Mixed TOC detected, using extracted main TOC (${validMarkdown.length} chars)`
4419
- );
4420
- }
4421
- markdown = validMarkdown;
4422
- this.logger.info(
4423
- `[DocumentProcessor] TOC validation passed (confidence: ${validation.confidence})`
4424
- );
4425
- } else {
4426
- markdown = null;
4427
- }
4428
- }
4429
- } catch (error) {
4430
- if (error instanceof TocNotFoundError) {
4431
- this.logger.info(
4432
- "[DocumentProcessor] Rule-based TOC not found, will try vision fallback"
4433
- );
4434
- } else {
4435
- throw error;
4436
- }
4437
- }
4438
- let fromVision = false;
4439
- if (!markdown) {
4440
- fromVision = true;
4441
- this.logger.info("[DocumentProcessor] Using vision fallback for TOC");
4442
- const totalPages2 = Object.keys(doclingDoc.pages).length;
4443
- markdown = await this.visionTocExtractor.extract(totalPages2);
4444
- if (!markdown) {
4445
- const reason = "Both rule-based search and vision fallback failed to locate TOC";
4446
- this.logger.error(
4447
- `[DocumentProcessor] TOC extraction failed: ${reason}`
4448
- );
4449
- throw new TocNotFoundError(
4450
- `Table of contents not found in the document. ${reason}.`
4451
- );
4452
- }
4453
- this.logger.info(
4454
- `[DocumentProcessor] Vision extracted TOC markdown (${markdown.length} chars)`
4455
- );
4456
- }
4457
- const totalPages = Object.keys(doclingDoc.pages).length;
4458
- const maxTocPageNo = this.extractMaxPageNumber(markdown);
4459
- const effectiveTotalPages = maxTocPageNo > totalPages ? void 0 : totalPages;
4460
- let tocResult;
4461
- try {
4462
- tocResult = await this.tocExtractor.extract(markdown, {
4463
- totalPages: effectiveTotalPages
4464
- });
4465
- } catch (error) {
4466
- if (error instanceof TocValidationError) {
4467
- this.logger.warn(
4468
- `[DocumentProcessor] TOC extraction validation failed: ${error.message}`
4469
- );
4470
- tocResult = { entries: [], usages: [] };
4471
- } else {
4472
- throw error;
4473
- }
4474
- }
4475
- for (const usage of tocResult.usages) {
4476
- this.usageAggregator.track(usage);
4477
- }
4478
- if (tocResult.entries.length === 0 && !fromVision) {
4479
- this.logger.warn(
4480
- "[DocumentProcessor] Text-based TOC extraction yielded 0 entries, retrying with vision"
4481
- );
4482
- const visionMarkdown = await this.visionTocExtractor.extract(totalPages);
4483
- if (visionMarkdown) {
4484
- this.logger.info(
4485
- `[DocumentProcessor] Vision extracted TOC markdown (${visionMarkdown.length} chars)`
4486
- );
4487
- const visionMaxPageNo = this.extractMaxPageNumber(visionMarkdown);
4488
- const visionEffectivePages = visionMaxPageNo > totalPages ? void 0 : totalPages;
4489
- try {
4490
- const visionResult = await this.tocExtractor.extract(
4491
- visionMarkdown,
4492
- {
4493
- totalPages: visionEffectivePages
4494
- }
4495
- );
4496
- for (const usage of visionResult.usages) {
4497
- this.usageAggregator.track(usage);
4498
- }
4499
- if (visionResult.entries.length > 0) {
4500
- tocResult = visionResult;
4501
- }
4502
- } catch {
4503
- }
4504
- }
4505
- }
4506
- if (tocResult.entries.length === 0) {
4507
- const reason = "TOC area was detected but LLM could not extract any structured entries";
4508
- this.logger.error(`[DocumentProcessor] TOC extraction failed: ${reason}`);
4509
- throw new TocNotFoundError(`${reason}.`);
4510
- }
4511
- this.logger.info(
4512
- `[DocumentProcessor] Extracted ${tocResult.entries.length} top-level TOC entries`
4513
- );
4514
- return tocResult.entries;
4515
- }
4516
- /**
4517
- * Extract the maximum page number from TOC markdown
4518
- *
4519
- * Parses page numbers from dot-leader patterns (e.g., "..... 175")
4520
- * and table cell patterns (e.g., "| title | 175 |") to detect compiled
4521
- * volume scenarios where TOC page numbers exceed the sub-document's page count.
4522
- */
4523
- extractMaxPageNumber(markdown) {
4524
- const dotLeaderMatches = [...markdown.matchAll(/\.{2,}\s*(\d+)/g)];
4525
- const tableCellMatches = [...markdown.matchAll(/\|\s*(\d+)\s*\|\s*$/gm)];
4526
- const allNumbers = [
4527
- ...dotLeaderMatches.map((m) => parseInt(m[1], 10)),
4528
- ...tableCellMatches.map((m) => parseInt(m[1], 10))
4529
- ];
4530
- if (allNumbers.length === 0) {
4531
- return 0;
4532
- }
4533
- return Math.max(...allNumbers);
4534
- }
4535
- /**
4536
- * Process resource captions (for images and tables)
4537
- *
4538
- * Common caption processing pipeline:
4539
- * 1. Parse captions in batch
4540
- * 2. Validate parsed captions
4541
- * 3. Reparse failed captions with fallback model
4542
- *
4543
- * @param captionTexts - Array of caption texts to process
4544
- * @param resourceType - Type of resource for logging (e.g., 'image', 'table')
4545
- * @returns Parsed captions with index mapping
4546
- */
4547
- async processResourceCaptions(captionTexts, resourceType) {
4548
- const captionsByIndex = /* @__PURE__ */ new Map();
4549
- const validCaptionData = [];
4550
- for (let i = 0; i < captionTexts.length; i++) {
4551
- const text = captionTexts[i];
4552
- if (text !== void 0) {
4553
- validCaptionData.push({
4554
- resourceIndex: i,
4555
- filteredIndex: validCaptionData.length,
4556
- text
4557
- });
4558
- }
4559
- }
4560
- const validCaptionTexts = validCaptionData.map((item) => item.text);
4561
- const parsedCaptions = validCaptionTexts.length > 0 ? await this.captionParser.parseBatch(
4562
- validCaptionTexts,
4563
- this.captionParserBatchSize
4564
- ) : [];
4565
- let finalValidCaptionData = validCaptionData;
4566
- let finalParsedCaptions = parsedCaptions;
4567
- if (parsedCaptions.length !== validCaptionData.length) {
4568
- this.logger.warn(
4569
- `[DocumentProcessor] Caption parsing length mismatch for ${resourceType}: expected ${validCaptionData.length}, got ${parsedCaptions.length}. Attempting recovery by matching fullText...`
4570
- );
4571
- const parsedMap = /* @__PURE__ */ new Map();
4572
- for (const parsed of parsedCaptions) {
4573
- parsedMap.set(parsed.fullText, parsed);
4574
- }
4575
- const recoveredData = [];
4576
- for (const item of validCaptionData) {
4577
- if (parsedMap.has(item.text)) {
4578
- recoveredData.push(item);
4579
- } else {
4580
- this.logger.warn(
4581
- `[DocumentProcessor] Skipping ${resourceType} caption at index ${item.resourceIndex}: "${item.text}" (not found in parsed results)`
4582
- );
4583
- }
4584
- }
4585
- const recoveredCaptions = [];
4586
- for (const item of recoveredData) {
4587
- const caption = parsedMap.get(item.text);
4588
- if (caption) {
4589
- recoveredCaptions.push(caption);
4590
- }
4591
- }
4592
- if (recoveredCaptions.length !== recoveredData.length) {
4593
- throw new Error(
4594
- `[DocumentProcessor] Failed to recover from length mismatch: recovered ${recoveredCaptions.length} captions for ${recoveredData.length} valid items`
4595
- );
4596
- }
4597
- finalValidCaptionData = recoveredData;
4598
- finalParsedCaptions = recoveredCaptions;
4599
- this.logger.info(
4600
- `[DocumentProcessor] Successfully recovered ${finalParsedCaptions.length} ${resourceType} captions after length mismatch`
4601
- );
4602
- }
4603
- for (let i = 0; i < finalParsedCaptions.length; i++) {
4604
- const resourceIndex = finalValidCaptionData[i].resourceIndex;
4605
- captionsByIndex.set(resourceIndex, finalParsedCaptions[i]);
4606
- }
4607
- if (finalParsedCaptions.length > 0) {
4608
- const finalValidCaptionTexts = finalValidCaptionData.map(
4609
- (item) => item.text
4610
- );
4611
- const validationResults = await this.captionValidator.validateBatch(
4612
- finalParsedCaptions,
4613
- finalValidCaptionTexts,
4614
- this.captionValidatorBatchSize
4615
- );
4616
- const failedIndices = validationResults.map((isValid, index) => isValid ? -1 : index).filter((index) => index !== -1);
4617
- if (failedIndices.length > 0) {
4618
- for (const filteredIndex of failedIndices) {
4619
- const captionData = finalValidCaptionData[filteredIndex];
4620
- const originalText = captionData.text;
4621
- const parsedNum = finalParsedCaptions[filteredIndex].num;
4622
- const resourceIndex = captionData.resourceIndex;
4623
- this.logger.warn(
4624
- `[DocumentProcessor] Invalid ${resourceType} caption [${resourceIndex}]: "${originalText}" | parsed num="${parsedNum}"`
4625
- );
4626
- }
4627
- if (this.enableFallbackRetry) {
4628
- this.logger.info(
4629
- `[DocumentProcessor] Reparsing ${failedIndices.length} failed ${resourceType} captions with fallback model...`
4630
- );
4631
- const failedCaptionTexts = failedIndices.map(
4632
- (filteredIndex) => finalValidCaptionData[filteredIndex].text
4633
- );
4634
- const fallbackCaptionParser = new CaptionParser(
4635
- this.logger,
4636
- this.fallbackModel,
4637
- {
4638
- maxRetries: this.maxRetries,
4639
- componentName: "CaptionParser-fallback",
4640
- abortSignal: this.abortSignal
4641
- },
4642
- void 0,
4643
- // no fallback for the fallback
4644
- this.usageAggregator
4645
- );
4646
- const reparsedCaptions = await fallbackCaptionParser.parseBatch(
4647
- failedCaptionTexts,
4648
- 0
4649
- // sequential processing
4650
- );
4651
- for (let i = 0; i < failedIndices.length; i++) {
4652
- const filteredIndex = failedIndices[i];
4653
- const resourceIndex = finalValidCaptionData[filteredIndex].resourceIndex;
4654
- captionsByIndex.set(resourceIndex, reparsedCaptions[i]);
4655
- }
4656
- this.logger.info(
4657
- `[DocumentProcessor] Reparsed ${reparsedCaptions.length} ${resourceType} captions`
4658
- );
4659
- } else {
4660
- this.logger.warn(
4661
- `[DocumentProcessor] ${failedIndices.length} ${resourceType} captions failed validation (kept as-is, fallback retry disabled)`
4662
- );
4663
- }
4664
- }
4665
- }
4666
- return captionsByIndex;
4667
- }
4668
- /**
4669
- * Extract caption text from resource
4670
- *
4671
- * Handles both string references and $ref resolution
4672
- */
4673
- extractCaptionText(captions) {
4674
- if (!captions?.[0]) {
4675
- return void 0;
4676
- }
4677
- const captionRef = captions[0];
4678
- if (typeof captionRef === "string") {
4679
- return captionRef;
4680
- }
4681
- if (this.refResolver && "$ref" in captionRef) {
4682
- const resolved = this.refResolver.resolveText(captionRef.$ref);
4683
- return resolved?.text;
4684
- }
4685
- return void 0;
4686
- }
4687
- /**
4688
- * Convert images
4689
- *
4690
- * Converts pictures from DoclingDocument to ProcessedImage
4691
- */
4692
- async convertImages(doclingDoc, outputPath) {
4693
- this.logger.info(
4694
- `[DocumentProcessor] Converting ${doclingDoc.pictures.length} images...`
4695
- );
4696
- const images = [];
4697
- const captionTexts = [];
4698
- for (const picture of doclingDoc.pictures) {
4699
- const pdfPageNo = picture.prov?.[0]?.page_no ?? 0;
4700
- const imageId = this.idGenerator?.generateImageId() ?? `img-${images.length + 1}`;
4701
- const captionText = this.extractCaptionText(picture.captions);
4702
- captionTexts.push(captionText);
4703
- images.push({
4704
- id: imageId,
4705
- path: `${outputPath}/images/image_${images.length}.png`,
4706
- pdfPageNo
4707
- // caption will be assigned later
4708
- });
4709
- }
4710
- const captionsByIndex = await this.processResourceCaptions(
4711
- captionTexts,
4712
- "image"
4713
- );
4714
- for (let i = 0; i < images.length; i++) {
4715
- if (captionsByIndex.has(i)) {
4716
- images[i].caption = captionsByIndex.get(i);
4717
- }
4718
- }
4719
- return images;
4720
- }
4721
- /**
4722
- * Convert tables
4723
- *
4724
- * Converts tables from DoclingDocument to ProcessedTable
4725
- */
4726
- async convertTables(doclingDoc) {
4727
- this.logger.info(
4728
- `[DocumentProcessor] Converting ${doclingDoc.tables.length} tables...`
4729
- );
4730
- const tables = [];
4731
- const captionTexts = [];
4732
- for (const table of doclingDoc.tables) {
4733
- const pdfPageNo = table.prov?.[0]?.page_no ?? 0;
4734
- const tableId = this.idGenerator?.generateTableId() ?? `tbl-${tables.length + 1}`;
4735
- const grid = table.data.grid.map(
4736
- (row) => row.map((cell) => ({
4737
- text: cell.text,
4738
- rowSpan: cell.row_span ?? 1,
4739
- colSpan: cell.col_span ?? 1,
4740
- isHeader: cell.column_header || cell.row_header || false
4741
- }))
4742
- );
4743
- const captionText = this.extractCaptionText(table.captions);
4744
- captionTexts.push(captionText);
4745
- tables.push({
4746
- id: tableId,
4747
- pdfPageNo,
4748
- numRows: grid.length,
4749
- numCols: grid[0]?.length ?? 0,
4750
- grid
4751
- // caption will be assigned later
4752
- });
4753
- }
4754
- const captionsByIndex = await this.processResourceCaptions(
4755
- captionTexts,
4756
- "table"
4757
- );
4758
- for (let i = 0; i < tables.length; i++) {
4759
- if (captionsByIndex.has(i)) {
4760
- tables[i].caption = captionsByIndex.get(i);
4761
- }
4762
- }
4763
- return tables;
4764
- }
4765
4722
  /**
4766
4723
  * Convert chapters and link resources
4767
4724
  *
@@ -4796,6 +4753,7 @@ var DocumentProcessor = class {
4796
4753
  CONTINUATION_MARKERS,
4797
4754
  CaptionParseError,
4798
4755
  CaptionParser,
4756
+ CaptionProcessingPipeline,
4799
4757
  CaptionValidationError,
4800
4758
  CaptionValidator,
4801
4759
  ChapterConverter,
@@ -4804,12 +4762,14 @@ var DocumentProcessor = class {
4804
4762
  PagePattern,
4805
4763
  PageRangeParseError,
4806
4764
  PageRangeParser,
4765
+ ResourceConverter,
4807
4766
  TOC_KEYWORDS,
4808
4767
  TextLLMComponent,
4809
4768
  TocContentValidationSchema,
4810
4769
  TocContentValidator,
4811
4770
  TocEntrySchema,
4812
4771
  TocExtractError,
4772
+ TocExtractionPipeline,
4813
4773
  TocExtractor,
4814
4774
  TocFinder,
4815
4775
  TocNotFoundError,
@@ -4817,6 +4777,7 @@ var DocumentProcessor = class {
4817
4777
  TocResponseSchema,
4818
4778
  VisionLLMComponent,
4819
4779
  VisionTocExtractionSchema,
4820
- VisionTocExtractor
4780
+ VisionTocExtractor,
4781
+ extractMaxPageNumber
4821
4782
  });
4822
4783
  //# sourceMappingURL=index.cjs.map