@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.js CHANGED
@@ -549,11 +549,7 @@ var RefResolver = class {
549
549
  * Build an index mapping self_ref to the actual item
550
550
  */
551
551
  buildIndex(items, _prefix) {
552
- const map = /* @__PURE__ */ new Map();
553
- for (const item of items) {
554
- map.set(item.self_ref, item);
555
- }
556
- return map;
552
+ return new Map(items.map((item) => [item.self_ref, item]));
557
553
  }
558
554
  /**
559
555
  * Resolve a $ref string to the actual item
@@ -779,14 +775,7 @@ var TextCleaner = class {
779
775
  */
780
776
  static normalizeAndFilterBatch(texts, batchSize = 10) {
781
777
  if (batchSize === 0) {
782
- const results = [];
783
- for (const text of texts) {
784
- const normalized = this.normalize(text);
785
- if (this.isValidText(normalized)) {
786
- results.push(normalized);
787
- }
788
- }
789
- return results;
778
+ return texts.map((text) => this.normalize(text)).filter((normalized) => this.isValidText(normalized));
790
779
  }
791
780
  return BatchProcessor.processBatchSync(texts, batchSize, (batch) => {
792
781
  const normalized = this.normalizeBatch(batch);
@@ -808,38 +797,22 @@ var MarkdownConverter = class _MarkdownConverter {
808
797
  if (refs.length === 0) {
809
798
  return "";
810
799
  }
811
- const lines = [];
812
- for (const ref of refs) {
813
- const item = refResolver.resolve(ref);
814
- if (!item) {
815
- continue;
816
- }
800
+ const lines = refs.map((ref) => refResolver.resolve(ref)).filter((item) => item !== null).map((item) => {
817
801
  if ("name" in item && (item.name === "list" || item.name === "group")) {
818
- const groupMarkdown = _MarkdownConverter.groupToMarkdown(
802
+ return _MarkdownConverter.groupToMarkdown(
819
803
  item,
820
804
  refResolver,
821
805
  0
822
806
  );
823
- if (groupMarkdown) {
824
- lines.push(groupMarkdown);
825
- }
826
- } else if ("data" in item && "grid" in item.data) {
827
- const tableMarkdown = _MarkdownConverter.tableToMarkdown(
828
- item
829
- );
830
- if (tableMarkdown) {
831
- lines.push(tableMarkdown);
832
- }
833
- } else if ("text" in item && "orig" in item) {
834
- const textMarkdown = _MarkdownConverter.textToMarkdown(
835
- item,
836
- 0
837
- );
838
- if (textMarkdown) {
839
- lines.push(textMarkdown);
840
- }
841
807
  }
842
- }
808
+ if ("data" in item && "grid" in item.data) {
809
+ return _MarkdownConverter.tableToMarkdown(item);
810
+ }
811
+ if ("text" in item && "orig" in item) {
812
+ return _MarkdownConverter.textToMarkdown(item, 0);
813
+ }
814
+ return "";
815
+ }).filter((line) => line !== "");
843
816
  return lines.join("\n\n");
844
817
  }
845
818
  /**
@@ -855,31 +828,22 @@ var MarkdownConverter = class _MarkdownConverter {
855
828
  * - Chapter 2 Methodology ..... 10
856
829
  */
857
830
  static groupToMarkdown(group, refResolver, indentLevel = 0) {
858
- const lines = [];
859
- for (const childRef of group.children) {
860
- const child = refResolver.resolve(childRef.$ref);
861
- if (!child) {
862
- continue;
863
- }
831
+ const lines = group.children.map((childRef) => refResolver.resolve(childRef.$ref)).filter((child) => child !== null).map((child) => {
864
832
  if ("name" in child && (child.name === "list" || child.name === "group")) {
865
- const nestedMarkdown = _MarkdownConverter.groupToMarkdown(
833
+ return _MarkdownConverter.groupToMarkdown(
866
834
  child,
867
835
  refResolver,
868
836
  indentLevel + 1
869
837
  );
870
- if (nestedMarkdown) {
871
- lines.push(nestedMarkdown);
872
- }
873
- } else if ("text" in child && "orig" in child) {
874
- const textMarkdown = _MarkdownConverter.textToMarkdown(
838
+ }
839
+ if ("text" in child && "orig" in child) {
840
+ return _MarkdownConverter.textToMarkdown(
875
841
  child,
876
842
  indentLevel
877
843
  );
878
- if (textMarkdown) {
879
- lines.push(textMarkdown);
880
- }
881
844
  }
882
- }
845
+ return "";
846
+ }).filter((line) => line !== "");
883
847
  return lines.join("\n");
884
848
  }
885
849
  /**
@@ -897,21 +861,20 @@ var MarkdownConverter = class _MarkdownConverter {
897
861
  if (!grid || grid.length === 0) {
898
862
  return "";
899
863
  }
900
- const lines = [];
901
- for (let rowIdx = 0; rowIdx < grid.length; rowIdx++) {
902
- const row = grid[rowIdx];
864
+ const lines = grid.flatMap((row, rowIdx) => {
903
865
  if (!row || row.length === 0) {
904
- continue;
866
+ return [];
905
867
  }
906
868
  const cells = row.map(
907
869
  (cell) => _MarkdownConverter.escapeTableCell(cell.text)
908
870
  );
909
- lines.push(`| ${cells.join(" | ")} |`);
871
+ const rowLine = `| ${cells.join(" | ")} |`;
910
872
  if (rowIdx === 0) {
911
873
  const separator = row.map(() => "---").join(" | ");
912
- lines.push(`| ${separator} |`);
874
+ return [rowLine, `| ${separator} |`];
913
875
  }
914
- }
876
+ return [rowLine];
877
+ });
915
878
  return lines.join("\n");
916
879
  }
917
880
  /**
@@ -958,6 +921,20 @@ var MarkdownConverter = class _MarkdownConverter {
958
921
  }
959
922
  };
960
923
 
924
+ // src/utils/toc-markdown-utils.ts
925
+ function extractMaxPageNumber(markdown) {
926
+ const dotLeaderMatches = [...markdown.matchAll(/\.{2,}\s*(\d+)/g)];
927
+ const tableCellMatches = [...markdown.matchAll(/\|\s*(\d+)\s*\|\s*$/gm)];
928
+ const allNumbers = [
929
+ ...dotLeaderMatches.map((m) => parseInt(m[1], 10)),
930
+ ...tableCellMatches.map((m) => parseInt(m[1], 10))
931
+ ];
932
+ if (allNumbers.length === 0) {
933
+ return 0;
934
+ }
935
+ return Math.max(...allNumbers);
936
+ }
937
+
961
938
  // src/converters/chapter-converter.ts
962
939
  var ChapterConverter = class _ChapterConverter {
963
940
  static FRONT_MATTER_ID = "ch-000";
@@ -1057,7 +1034,7 @@ var ChapterConverter = class _ChapterConverter {
1057
1034
  flattenChapters(chapters) {
1058
1035
  const result = [];
1059
1036
  const flatten = (chapterList) => {
1060
- for (const chapter of chapterList) {
1037
+ chapterList.forEach((chapter) => {
1061
1038
  result.push({
1062
1039
  chapter,
1063
1040
  tocPageNo: chapter.pageNo
@@ -1065,7 +1042,7 @@ var ChapterConverter = class _ChapterConverter {
1065
1042
  if (chapter.children && chapter.children.length > 0) {
1066
1043
  flatten(chapter.children);
1067
1044
  }
1068
- }
1045
+ });
1069
1046
  };
1070
1047
  flatten(chapters);
1071
1048
  return result;
@@ -1092,14 +1069,13 @@ var ChapterConverter = class _ChapterConverter {
1092
1069
  startPage: 1,
1093
1070
  endPage: firstTocPage - 1
1094
1071
  });
1095
- for (let i = 0; i < sorted.length; i++) {
1096
- const current = sorted[i];
1072
+ sorted.forEach((current, i) => {
1097
1073
  const next = sorted[i + 1];
1098
1074
  ranges.set(current.chapter.id, {
1099
1075
  startPage: current.tocPageNo,
1100
1076
  endPage: next ? next.tocPageNo - 1 : Number.MAX_SAFE_INTEGER
1101
1077
  });
1102
- }
1078
+ });
1103
1079
  return ranges;
1104
1080
  }
1105
1081
  /**
@@ -1168,7 +1144,7 @@ var ChapterConverter = class _ChapterConverter {
1168
1144
  */
1169
1145
  assignTextBlocks(chapters, textBlocks, chapterRanges, pageRangeMap) {
1170
1146
  const chapterMap = this.buildChapterMap(chapters);
1171
- for (const textBlock of textBlocks) {
1147
+ textBlocks.forEach((textBlock) => {
1172
1148
  const actualPageNo = this.pdfPageToActualPage(
1173
1149
  textBlock.pdfPageNo,
1174
1150
  pageRangeMap
@@ -1177,14 +1153,14 @@ var ChapterConverter = class _ChapterConverter {
1177
1153
  if (chapterId && chapterMap.has(chapterId)) {
1178
1154
  chapterMap.get(chapterId).textBlocks.push(textBlock);
1179
1155
  }
1180
- }
1156
+ });
1181
1157
  }
1182
1158
  /**
1183
1159
  * Link images, tables, and footnotes to chapters based on page ranges
1184
1160
  */
1185
1161
  linkResources(chapters, images, tables, footnotes, chapterRanges, pageRangeMap) {
1186
1162
  const chapterMap = this.buildChapterMap(chapters);
1187
- for (const image of images) {
1163
+ images.forEach((image) => {
1188
1164
  const actualPageNo = this.pdfPageToActualPage(
1189
1165
  image.pdfPageNo,
1190
1166
  pageRangeMap
@@ -1193,8 +1169,8 @@ var ChapterConverter = class _ChapterConverter {
1193
1169
  if (chapterId && chapterMap.has(chapterId)) {
1194
1170
  chapterMap.get(chapterId).imageIds.push(image.id);
1195
1171
  }
1196
- }
1197
- for (const table of tables) {
1172
+ });
1173
+ tables.forEach((table) => {
1198
1174
  const actualPageNo = this.pdfPageToActualPage(
1199
1175
  table.pdfPageNo,
1200
1176
  pageRangeMap
@@ -1203,8 +1179,8 @@ var ChapterConverter = class _ChapterConverter {
1203
1179
  if (chapterId && chapterMap.has(chapterId)) {
1204
1180
  chapterMap.get(chapterId).tableIds.push(table.id);
1205
1181
  }
1206
- }
1207
- for (const footnote of footnotes) {
1182
+ });
1183
+ footnotes.forEach((footnote) => {
1208
1184
  const actualPageNo = this.pdfPageToActualPage(
1209
1185
  footnote.pdfPageNo,
1210
1186
  pageRangeMap
@@ -1213,7 +1189,7 @@ var ChapterConverter = class _ChapterConverter {
1213
1189
  if (chapterId && chapterMap.has(chapterId)) {
1214
1190
  chapterMap.get(chapterId).footnoteIds.push(footnote.id);
1215
1191
  }
1216
- }
1192
+ });
1217
1193
  }
1218
1194
  /**
1219
1195
  * Build flat chapter map for O(1) lookup
@@ -1221,18 +1197,135 @@ var ChapterConverter = class _ChapterConverter {
1221
1197
  buildChapterMap(chapters) {
1222
1198
  const map = /* @__PURE__ */ new Map();
1223
1199
  const addToMap = (chapterList) => {
1224
- for (const chapter of chapterList) {
1200
+ chapterList.forEach((chapter) => {
1225
1201
  map.set(chapter.id, chapter);
1226
1202
  if (chapter.children && chapter.children.length > 0) {
1227
1203
  addToMap(chapter.children);
1228
1204
  }
1229
- }
1205
+ });
1230
1206
  };
1231
1207
  addToMap(chapters);
1232
1208
  return map;
1233
1209
  }
1234
1210
  };
1235
1211
 
1212
+ // src/converters/resource-converter.ts
1213
+ var ResourceConverter = class {
1214
+ logger;
1215
+ idGenerator;
1216
+ captionProcessingPipeline;
1217
+ constructor(logger, idGenerator, captionProcessingPipeline) {
1218
+ this.logger = logger;
1219
+ this.idGenerator = idGenerator;
1220
+ this.captionProcessingPipeline = captionProcessingPipeline;
1221
+ }
1222
+ /**
1223
+ * Convert all resources (images, tables, footnotes)
1224
+ *
1225
+ * Runs image and table conversions in parallel, then footnotes synchronously.
1226
+ */
1227
+ async convertAll(doclingDoc, outputPath) {
1228
+ this.logger.info(
1229
+ "[ResourceConverter] Converting images, tables, and footnotes..."
1230
+ );
1231
+ const [images, tables] = await Promise.all([
1232
+ this.convertImages(doclingDoc, outputPath),
1233
+ this.convertTables(doclingDoc)
1234
+ ]);
1235
+ const footnotes = this.convertFootnotes(doclingDoc);
1236
+ this.logger.info(
1237
+ `[ResourceConverter] Converted ${images.length} images, ${tables.length} tables, and ${footnotes.length} footnotes`
1238
+ );
1239
+ return { images, tables, footnotes };
1240
+ }
1241
+ /**
1242
+ * Convert images from DoclingDocument to ProcessedImage[]
1243
+ */
1244
+ async convertImages(doclingDoc, outputPath) {
1245
+ this.logger.info(
1246
+ `[ResourceConverter] Converting ${doclingDoc.pictures.length} images...`
1247
+ );
1248
+ const captionTexts = doclingDoc.pictures.map(
1249
+ (picture) => this.captionProcessingPipeline.extractCaptionText(picture.captions)
1250
+ );
1251
+ const images = doclingDoc.pictures.map(
1252
+ (picture, index) => ({
1253
+ id: this.idGenerator.generateImageId(),
1254
+ path: `${outputPath}/images/image_${index}.png`,
1255
+ pdfPageNo: picture.prov?.[0]?.page_no ?? 0
1256
+ })
1257
+ );
1258
+ const captionsByIndex = await this.captionProcessingPipeline.processResourceCaptions(
1259
+ captionTexts,
1260
+ "image"
1261
+ );
1262
+ images.forEach((image, i) => {
1263
+ if (captionsByIndex.has(i)) {
1264
+ image.caption = captionsByIndex.get(i);
1265
+ }
1266
+ });
1267
+ return images;
1268
+ }
1269
+ /**
1270
+ * Convert tables from DoclingDocument to ProcessedTable[]
1271
+ */
1272
+ async convertTables(doclingDoc) {
1273
+ this.logger.info(
1274
+ `[ResourceConverter] Converting ${doclingDoc.tables.length} tables...`
1275
+ );
1276
+ const captionTexts = doclingDoc.tables.map(
1277
+ (table) => this.captionProcessingPipeline.extractCaptionText(table.captions)
1278
+ );
1279
+ const tables = doclingDoc.tables.map((table) => {
1280
+ const grid = table.data.grid.map(
1281
+ (row) => row.map((cell) => ({
1282
+ text: cell.text,
1283
+ rowSpan: cell.row_span ?? 1,
1284
+ colSpan: cell.col_span ?? 1,
1285
+ isHeader: cell.column_header || cell.row_header || false
1286
+ }))
1287
+ );
1288
+ return {
1289
+ id: this.idGenerator.generateTableId(),
1290
+ pdfPageNo: table.prov?.[0]?.page_no ?? 0,
1291
+ numRows: grid.length,
1292
+ numCols: grid[0]?.length ?? 0,
1293
+ grid
1294
+ };
1295
+ });
1296
+ const captionsByIndex = await this.captionProcessingPipeline.processResourceCaptions(
1297
+ captionTexts,
1298
+ "table"
1299
+ );
1300
+ tables.forEach((table, i) => {
1301
+ if (captionsByIndex.has(i)) {
1302
+ table.caption = captionsByIndex.get(i);
1303
+ }
1304
+ });
1305
+ return tables;
1306
+ }
1307
+ /**
1308
+ * Convert footnotes from DoclingDocument text items
1309
+ */
1310
+ convertFootnotes(doclingDoc) {
1311
+ const footnoteItems = doclingDoc.texts.filter(
1312
+ (item) => item.label === "footnote"
1313
+ );
1314
+ this.logger.info(
1315
+ `[ResourceConverter] Converting ${footnoteItems.length} footnotes...`
1316
+ );
1317
+ const footnotes = footnoteItems.filter((item) => TextCleaner.isValidText(item.text)).map((item) => ({
1318
+ id: this.idGenerator.generateFootnoteId(),
1319
+ text: TextCleaner.normalize(item.text),
1320
+ pdfPageNo: item.prov?.[0]?.page_no ?? 1
1321
+ }));
1322
+ this.logger.info(
1323
+ `[ResourceConverter] Converted ${footnotes.length} valid footnotes`
1324
+ );
1325
+ return footnotes;
1326
+ }
1327
+ };
1328
+
1236
1329
  // src/extractors/toc-extract-error.ts
1237
1330
  var TocExtractError = class _TocExtractError extends Error {
1238
1331
  constructor(message, options) {
@@ -1614,43 +1707,33 @@ var TocFinder = class {
1614
1707
  */
1615
1708
  findByStructure(doc) {
1616
1709
  const candidates = [];
1617
- for (const group of doc.groups) {
1618
- const pageNo = this.getGroupFirstPage(group);
1619
- if (pageNo === void 0 || pageNo > this.maxSearchPages) {
1620
- continue;
1621
- }
1622
- if (this.isGroupTocLike(group, doc)) {
1623
- const score = this.calculateScore(group, pageNo);
1624
- candidates.push({
1625
- result: {
1626
- itemRefs: [group.self_ref],
1627
- startPage: pageNo,
1628
- endPage: pageNo
1629
- },
1630
- score
1631
- });
1632
- }
1633
- }
1634
- for (const table of doc.tables) {
1710
+ doc.groups.map((group) => ({ group, pageNo: this.getGroupFirstPage(group) })).filter(
1711
+ ({ group, pageNo }) => pageNo !== void 0 && pageNo <= this.maxSearchPages && this.isGroupTocLike(group, doc)
1712
+ ).forEach(({ group, pageNo }) => {
1713
+ candidates.push({
1714
+ result: {
1715
+ itemRefs: [group.self_ref],
1716
+ startPage: pageNo,
1717
+ endPage: pageNo
1718
+ },
1719
+ score: this.calculateScore(group, pageNo)
1720
+ });
1721
+ });
1722
+ doc.tables.filter((table) => {
1635
1723
  const pageNo = table.prov[0]?.page_no;
1636
- if (pageNo === void 0 || pageNo > this.maxSearchPages) {
1637
- continue;
1638
- }
1639
- if (this.isTableTocLike(table)) {
1640
- let score = this.calculateTableScore(table, pageNo);
1641
- if (this.isResourceIndexTable(table)) {
1642
- score -= 100;
1643
- }
1644
- candidates.push({
1645
- result: {
1646
- itemRefs: [table.self_ref],
1647
- startPage: pageNo,
1648
- endPage: pageNo
1649
- },
1650
- score
1651
- });
1652
- }
1653
- }
1724
+ return pageNo !== void 0 && pageNo <= this.maxSearchPages && this.isTableTocLike(table);
1725
+ }).forEach((table) => {
1726
+ const pageNo = table.prov[0].page_no;
1727
+ const score = this.calculateTableScore(table, pageNo) - (this.isResourceIndexTable(table) ? 100 : 0);
1728
+ candidates.push({
1729
+ result: {
1730
+ itemRefs: [table.self_ref],
1731
+ startPage: pageNo,
1732
+ endPage: pageNo
1733
+ },
1734
+ score
1735
+ });
1736
+ });
1654
1737
  if (candidates.length === 0) {
1655
1738
  return null;
1656
1739
  }
@@ -1722,17 +1805,10 @@ var TocFinder = class {
1722
1805
  if (group.name !== "list" && group.name !== "group") {
1723
1806
  return false;
1724
1807
  }
1725
- let pageNumberCount = 0;
1726
1808
  const children = this.refResolver.resolveMany(group.children);
1727
- for (const child of children) {
1728
- if (!child) continue;
1729
- if ("text" in child && "orig" in child) {
1730
- const textItem = child;
1731
- if (PAGE_NUMBER_PATTERN.test(textItem.text)) {
1732
- pageNumberCount++;
1733
- }
1734
- }
1735
- }
1809
+ const pageNumberCount = children.filter(
1810
+ (child) => child !== null && "text" in child && "orig" in child && PAGE_NUMBER_PATTERN.test(child.text)
1811
+ ).length;
1736
1812
  const total = children.filter((c) => c !== null).length;
1737
1813
  return pageNumberCount >= 3 || total > 0 && pageNumberCount / total > 0.5;
1738
1814
  }
@@ -1747,13 +1823,10 @@ var TocFinder = class {
1747
1823
  if (num_rows < 3 || num_cols < 2) {
1748
1824
  return false;
1749
1825
  }
1750
- let numberCount = 0;
1751
- for (let row = 1; row < grid.length; row++) {
1752
- const lastCell = grid[row]?.[num_cols - 1];
1753
- if (lastCell && /^\d+$/.test(lastCell.text.trim())) {
1754
- numberCount++;
1755
- }
1756
- }
1826
+ const numberCount = grid.slice(1).filter((row) => {
1827
+ const lastCell = row?.[num_cols - 1];
1828
+ return lastCell && /^\d+$/.test(lastCell.text.trim());
1829
+ }).length;
1757
1830
  return numberCount > 0 && numberCount / (num_rows - 1) > 0.5;
1758
1831
  }
1759
1832
  /**
@@ -1765,12 +1838,9 @@ var TocFinder = class {
1765
1838
  (c) => c.start_col_offset_idx === 0
1766
1839
  );
1767
1840
  if (firstColCells.length === 0) return false;
1768
- let resourceCount = 0;
1769
- for (const cell of firstColCells) {
1770
- if (RESOURCE_INDEX_PATTERNS.some((p) => p.test(cell.text.trim()))) {
1771
- resourceCount++;
1772
- }
1773
- }
1841
+ const resourceCount = firstColCells.filter(
1842
+ (cell) => RESOURCE_INDEX_PATTERNS.some((p) => p.test(cell.text.trim()))
1843
+ ).length;
1774
1844
  return resourceCount / firstColCells.length > 0.5;
1775
1845
  }
1776
1846
  /**
@@ -1787,9 +1857,7 @@ var TocFinder = class {
1787
1857
  break;
1788
1858
  }
1789
1859
  const newItems = continuationItems.filter((ref) => !seenRefs.has(ref));
1790
- for (const ref of newItems) {
1791
- seenRefs.add(ref);
1792
- }
1860
+ newItems.forEach((ref) => seenRefs.add(ref));
1793
1861
  itemRefs.unshift(...newItems);
1794
1862
  startPage = pageNo;
1795
1863
  this.logger.info(`[TocFinder] Expanded TOC backward to page ${pageNo}`);
@@ -1800,9 +1868,7 @@ var TocFinder = class {
1800
1868
  break;
1801
1869
  }
1802
1870
  const newItems = continuationItems.filter((ref) => !seenRefs.has(ref));
1803
- for (const ref of newItems) {
1804
- seenRefs.add(ref);
1805
- }
1871
+ newItems.forEach((ref) => seenRefs.add(ref));
1806
1872
  itemRefs.push(...newItems);
1807
1873
  endPage = pageNo;
1808
1874
  this.logger.info(`[TocFinder] Expanded TOC forward to page ${pageNo}`);
@@ -1818,37 +1884,27 @@ var TocFinder = class {
1818
1884
  */
1819
1885
  findContinuationOnPage(doc, pageNo) {
1820
1886
  const refs = [];
1821
- for (const text of doc.texts) {
1822
- if (text.prov[0]?.page_no !== pageNo) {
1823
- continue;
1824
- }
1825
- if (this.hasContinuationMarker(text.text)) {
1826
- const parentRef = text.parent?.$ref;
1827
- if (parentRef) {
1828
- const group = this.refResolver.resolveGroup(parentRef);
1829
- if (group) {
1830
- refs.push(group.self_ref);
1831
- }
1887
+ doc.texts.filter(
1888
+ (text) => text.prov[0]?.page_no === pageNo && this.hasContinuationMarker(text.text)
1889
+ ).forEach((text) => {
1890
+ const parentRef = text.parent?.$ref;
1891
+ if (parentRef) {
1892
+ const group = this.refResolver.resolveGroup(parentRef);
1893
+ if (group) {
1894
+ refs.push(group.self_ref);
1832
1895
  }
1833
1896
  }
1834
- }
1835
- for (const group of doc.groups) {
1836
- const groupPage = this.getGroupFirstPage(group);
1837
- if (groupPage !== pageNo) {
1838
- continue;
1839
- }
1840
- if (this.isGroupTocLike(group, doc) && !refs.includes(group.self_ref)) {
1841
- refs.push(group.self_ref);
1842
- }
1843
- }
1844
- for (const table of doc.tables) {
1845
- if (table.prov[0]?.page_no !== pageNo) {
1846
- continue;
1847
- }
1848
- if (this.isTableTocLike(table) && !this.isResourceIndexTable(table) && !refs.includes(table.self_ref)) {
1849
- refs.push(table.self_ref);
1850
- }
1851
- }
1897
+ });
1898
+ doc.groups.filter(
1899
+ (group) => this.getGroupFirstPage(group) === pageNo && this.isGroupTocLike(group, doc) && !refs.includes(group.self_ref)
1900
+ ).forEach((group) => {
1901
+ refs.push(group.self_ref);
1902
+ });
1903
+ doc.tables.filter(
1904
+ (table) => table.prov[0]?.page_no === pageNo && this.isTableTocLike(table) && !this.isResourceIndexTable(table) && !refs.includes(table.self_ref)
1905
+ ).forEach((table) => {
1906
+ refs.push(table.self_ref);
1907
+ });
1852
1908
  return refs;
1853
1909
  }
1854
1910
  /**
@@ -1873,16 +1929,10 @@ var TocFinder = class {
1873
1929
  * Get first page number of a group by checking its children
1874
1930
  */
1875
1931
  getGroupFirstPage(group) {
1876
- for (const childRef of group.children) {
1877
- const child = this.refResolver.resolve(childRef.$ref);
1878
- if (child && "prov" in child) {
1879
- const prov = child.prov;
1880
- if (prov && prov[0]?.page_no !== void 0) {
1881
- return prov[0].page_no;
1882
- }
1883
- }
1884
- }
1885
- return void 0;
1932
+ const found = group.children.map((childRef) => this.refResolver.resolve(childRef.$ref)).find(
1933
+ (child) => child !== null && "prov" in child && child.prov?.[0]?.page_no !== void 0
1934
+ );
1935
+ return found ? found.prov[0]?.page_no : void 0;
1886
1936
  }
1887
1937
  /**
1888
1938
  * Calculate score for a group candidate
@@ -1893,14 +1943,9 @@ var TocFinder = class {
1893
1943
  score += (this.maxSearchPages - pageNo + 1) * 10;
1894
1944
  score += group.children.length * 2;
1895
1945
  const children = this.refResolver.resolveMany(group.children);
1896
- for (const child of children) {
1897
- if (child && "text" in child) {
1898
- const textItem = child;
1899
- if (PAGE_NUMBER_PATTERN.test(textItem.text)) {
1900
- score += 5;
1901
- }
1902
- }
1903
- }
1946
+ score += children.filter(
1947
+ (child) => child !== null && "text" in child && PAGE_NUMBER_PATTERN.test(child.text)
1948
+ ).length * 5;
1904
1949
  return score;
1905
1950
  }
1906
1951
  /**
@@ -2978,9 +3023,7 @@ var PageRangeParser = class extends VisionLLMComponent {
2978
3023
  Object.assign(pageRangeMap, groupResult.pageRangeMap);
2979
3024
  usageList.push(...groupResult.usage);
2980
3025
  }
2981
- for (const usage of usageList) {
2982
- this.trackUsage(usage);
2983
- }
3026
+ usageList.forEach((usage) => this.trackUsage(usage));
2984
3027
  this.postProcess(pageRangeMap);
2985
3028
  this.log(
2986
3029
  "info",
@@ -3048,9 +3091,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3048
3091
  this.SAMPLE_SIZE,
3049
3092
  sampledPages
3050
3093
  );
3051
- for (const p of samplePageNos) {
3052
- sampledPages.add(p);
3053
- }
3094
+ samplePageNos.forEach((p) => sampledPages.add(p));
3054
3095
  this.log(
3055
3096
  "info",
3056
3097
  `Attempt ${attempt + 1}/${this.MAX_PATTERN_RETRIES + 1}: sampling pages ${samplePageNos.join(", ")}`
@@ -3110,10 +3151,9 @@ var PageRangeParser = class extends VisionLLMComponent {
3110
3151
  "warn",
3111
3152
  `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`
3112
3153
  );
3113
- const fallbackMap = {};
3114
- for (const pageNo of pageNos) {
3115
- fallbackMap[pageNo] = { startPageNo: 0, endPageNo: 0 };
3116
- }
3154
+ const fallbackMap = Object.fromEntries(
3155
+ pageNos.map((pageNo) => [pageNo, { startPageNo: 0, endPageNo: 0 }])
3156
+ );
3117
3157
  return { pageRangeMap: fallbackMap, usage: usageList };
3118
3158
  }
3119
3159
  /**
@@ -3135,7 +3175,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3135
3175
  async extractMultiplePages(pages, pageNos, model) {
3136
3176
  this.log("info", `Extracting ${pageNos.length} pages in single LLM call`);
3137
3177
  const imageContents = [];
3138
- for (const pageNo of pageNos) {
3178
+ const imageContentsFromPages = pageNos.map((pageNo) => {
3139
3179
  const page = pages[pageNo - 1];
3140
3180
  const imagePath = path3.resolve(
3141
3181
  this.outputPath,
@@ -3143,12 +3183,13 @@ var PageRangeParser = class extends VisionLLMComponent {
3143
3183
  );
3144
3184
  const imageData = new Uint8Array(fs3.readFileSync(imagePath));
3145
3185
  const mimeType = page.image.mimetype || "image/png";
3146
- imageContents.push({
3186
+ return {
3147
3187
  type: "image",
3148
3188
  image: imageData,
3149
3189
  mediaType: mimeType
3150
- });
3151
- }
3190
+ };
3191
+ });
3192
+ imageContents.push(...imageContentsFromPages);
3152
3193
  const schema = z4.object({
3153
3194
  pages: z4.array(
3154
3195
  z4.object({
@@ -3247,7 +3288,7 @@ var PageRangeParser = class extends VisionLLMComponent {
3247
3288
  */
3248
3289
  applyPattern(pageNos, pattern) {
3249
3290
  const result = {};
3250
- for (const pdfPageNo of pageNos) {
3291
+ pageNos.forEach((pdfPageNo) => {
3251
3292
  switch (pattern.pattern) {
3252
3293
  case "simple_increment" /* SIMPLE_INCREMENT */:
3253
3294
  case "offset" /* OFFSET */: {
@@ -3269,25 +3310,22 @@ var PageRangeParser = class extends VisionLLMComponent {
3269
3310
  default:
3270
3311
  result[pdfPageNo] = { startPageNo: 0, endPageNo: 0 };
3271
3312
  }
3272
- }
3313
+ });
3273
3314
  return result;
3274
3315
  }
3275
3316
  /**
3276
3317
  * Convert sample results to page range map (for small groups)
3277
3318
  */
3278
3319
  samplesToMap(samples) {
3279
- const result = {};
3280
- for (const sample of samples) {
3281
- if (sample.startPageNo !== null) {
3282
- result[sample.pdfPageNo] = {
3320
+ return Object.fromEntries(
3321
+ samples.map((sample) => [
3322
+ sample.pdfPageNo,
3323
+ sample.startPageNo !== null ? {
3283
3324
  startPageNo: sample.startPageNo,
3284
3325
  endPageNo: sample.endPageNo ?? sample.startPageNo
3285
- };
3286
- } else {
3287
- result[sample.pdfPageNo] = { startPageNo: 0, endPageNo: 0 };
3288
- }
3289
- }
3290
- return result;
3326
+ } : { startPageNo: 0, endPageNo: 0 }
3327
+ ])
3328
+ );
3291
3329
  }
3292
3330
  /**
3293
3331
  * Post-process the page range map
@@ -3574,42 +3612,357 @@ Remember: Look for SMALL numbers in MARGINS only. Ignore figure/table/photo numb
3574
3612
  }
3575
3613
  };
3576
3614
 
3577
- // src/validators/base-validator.ts
3578
- var BaseValidator = class extends TextLLMComponent {
3579
- /**
3580
- * Validator name for logging (kept for backwards compatibility)
3581
- */
3582
- validatorName;
3583
- /**
3584
- * Constructor for BaseValidator
3585
- *
3586
- * @param logger - Logger instance
3587
- * @param model - Language model to use for validation
3588
- * @param validatorName - Name of the validator for logging (e.g., "TocContentValidator")
3589
- * @param options - Optional configuration (maxRetries, temperature)
3590
- * @param fallbackModel - Optional fallback model for retry on failure
3591
- * @param aggregator - Optional token usage aggregator for tracking LLM calls
3592
- */
3593
- constructor(logger, model, validatorName, options, fallbackModel, aggregator) {
3594
- super(logger, model, validatorName, options, fallbackModel, aggregator);
3595
- this.validatorName = validatorName;
3615
+ // src/pipelines/caption-processing-pipeline.ts
3616
+ var CaptionProcessingPipeline = class {
3617
+ logger;
3618
+ captionParser;
3619
+ captionValidator;
3620
+ refResolver;
3621
+ fallbackModel;
3622
+ enableFallbackRetry;
3623
+ maxRetries;
3624
+ captionParserBatchSize;
3625
+ captionValidatorBatchSize;
3626
+ usageAggregator;
3627
+ abortSignal;
3628
+ constructor(deps) {
3629
+ this.logger = deps.logger;
3630
+ this.captionParser = deps.captionParser;
3631
+ this.captionValidator = deps.captionValidator;
3632
+ this.refResolver = deps.refResolver;
3633
+ this.fallbackModel = deps.fallbackModel;
3634
+ this.enableFallbackRetry = deps.enableFallbackRetry;
3635
+ this.maxRetries = deps.maxRetries;
3636
+ this.captionParserBatchSize = deps.captionParserBatchSize;
3637
+ this.captionValidatorBatchSize = deps.captionValidatorBatchSize;
3638
+ this.usageAggregator = deps.usageAggregator;
3639
+ this.abortSignal = deps.abortSignal;
3596
3640
  }
3597
3641
  /**
3598
- * Call LLM with LLMCaller
3642
+ * Process resource captions (for images and tables)
3599
3643
  *
3600
- * This method provides backwards compatibility with existing validators.
3601
- * It wraps the parent callTextLLM method but allows passing a custom aggregator.
3644
+ * Common caption processing pipeline:
3645
+ * 1. Parse captions in batch
3646
+ * 2. Validate parsed captions
3647
+ * 3. Reparse failed captions with fallback model
3602
3648
  *
3603
- * @param schema - Zod schema for response validation
3604
- * @param systemPrompt - System prompt
3605
- * @param userPrompt - User prompt
3606
- * @param phase - Phase name for tracking (e.g., 'validation', 'batch-validation')
3607
- * @param aggregator - Optional token usage aggregator for tracking this call
3608
- * @returns Parsed and validated LLM response with usage information
3649
+ * @param captionTexts - Array of caption texts to process
3650
+ * @param resourceType - Type of resource for logging (e.g., 'image', 'table')
3651
+ * @returns Parsed captions with index mapping
3609
3652
  */
3610
- async callLLM(schema, systemPrompt, userPrompt, phase, aggregator) {
3611
- const result = await LLMCaller.call({
3612
- schema,
3653
+ async processResourceCaptions(captionTexts, resourceType) {
3654
+ const captionsByIndex = /* @__PURE__ */ new Map();
3655
+ const validCaptionData = [];
3656
+ captionTexts.forEach((text, i) => {
3657
+ if (text !== void 0) {
3658
+ validCaptionData.push({
3659
+ resourceIndex: i,
3660
+ filteredIndex: validCaptionData.length,
3661
+ text
3662
+ });
3663
+ }
3664
+ });
3665
+ const validCaptionTexts = validCaptionData.map((item) => item.text);
3666
+ const parsedCaptions = validCaptionTexts.length > 0 ? await this.captionParser.parseBatch(
3667
+ validCaptionTexts,
3668
+ this.captionParserBatchSize
3669
+ ) : [];
3670
+ let finalValidCaptionData = validCaptionData;
3671
+ let finalParsedCaptions = parsedCaptions;
3672
+ if (parsedCaptions.length !== validCaptionData.length) {
3673
+ this.logger.warn(
3674
+ `[CaptionProcessingPipeline] Caption parsing length mismatch for ${resourceType}: expected ${validCaptionData.length}, got ${parsedCaptions.length}. Attempting recovery by matching fullText...`
3675
+ );
3676
+ const parsedMap = new Map(
3677
+ parsedCaptions.map((parsed) => [parsed.fullText, parsed])
3678
+ );
3679
+ const recoveredData = validCaptionData.filter((item) => {
3680
+ if (parsedMap.has(item.text)) {
3681
+ return true;
3682
+ }
3683
+ this.logger.warn(
3684
+ `[CaptionProcessingPipeline] Skipping ${resourceType} caption at index ${item.resourceIndex}: "${item.text}" (not found in parsed results)`
3685
+ );
3686
+ return false;
3687
+ });
3688
+ const recoveredCaptions = recoveredData.map((item) => parsedMap.get(item.text)).filter((caption) => caption !== void 0);
3689
+ if (recoveredCaptions.length !== recoveredData.length) {
3690
+ throw new Error(
3691
+ `[CaptionProcessingPipeline] Failed to recover from length mismatch: recovered ${recoveredCaptions.length} captions for ${recoveredData.length} valid items`
3692
+ );
3693
+ }
3694
+ finalValidCaptionData = recoveredData;
3695
+ finalParsedCaptions = recoveredCaptions;
3696
+ this.logger.info(
3697
+ `[CaptionProcessingPipeline] Successfully recovered ${finalParsedCaptions.length} ${resourceType} captions after length mismatch`
3698
+ );
3699
+ }
3700
+ finalParsedCaptions.forEach((caption, i) => {
3701
+ const resourceIndex = finalValidCaptionData[i].resourceIndex;
3702
+ captionsByIndex.set(resourceIndex, caption);
3703
+ });
3704
+ if (finalParsedCaptions.length > 0) {
3705
+ const finalValidCaptionTexts = finalValidCaptionData.map(
3706
+ (item) => item.text
3707
+ );
3708
+ const validationResults = await this.captionValidator.validateBatch(
3709
+ finalParsedCaptions,
3710
+ finalValidCaptionTexts,
3711
+ this.captionValidatorBatchSize
3712
+ );
3713
+ const failedIndices = validationResults.map((isValid, index) => isValid ? -1 : index).filter((index) => index !== -1);
3714
+ if (failedIndices.length > 0) {
3715
+ failedIndices.forEach((filteredIndex) => {
3716
+ const captionData = finalValidCaptionData[filteredIndex];
3717
+ const originalText = captionData.text;
3718
+ const parsedNum = finalParsedCaptions[filteredIndex].num;
3719
+ const resourceIndex = captionData.resourceIndex;
3720
+ this.logger.warn(
3721
+ `[CaptionProcessingPipeline] Invalid ${resourceType} caption [${resourceIndex}]: "${originalText}" | parsed num="${parsedNum}"`
3722
+ );
3723
+ });
3724
+ if (this.enableFallbackRetry) {
3725
+ this.logger.info(
3726
+ `[CaptionProcessingPipeline] Reparsing ${failedIndices.length} failed ${resourceType} captions with fallback model...`
3727
+ );
3728
+ const failedCaptionTexts = failedIndices.map(
3729
+ (filteredIndex) => finalValidCaptionData[filteredIndex].text
3730
+ );
3731
+ const fallbackCaptionParser = new CaptionParser(
3732
+ this.logger,
3733
+ this.fallbackModel,
3734
+ {
3735
+ maxRetries: this.maxRetries,
3736
+ componentName: "CaptionParser-fallback",
3737
+ abortSignal: this.abortSignal
3738
+ },
3739
+ void 0,
3740
+ // no fallback for the fallback
3741
+ this.usageAggregator
3742
+ );
3743
+ const reparsedCaptions = await fallbackCaptionParser.parseBatch(
3744
+ failedCaptionTexts,
3745
+ 0
3746
+ // sequential processing
3747
+ );
3748
+ failedIndices.forEach((filteredIndex, i) => {
3749
+ const resourceIndex = finalValidCaptionData[filteredIndex].resourceIndex;
3750
+ captionsByIndex.set(resourceIndex, reparsedCaptions[i]);
3751
+ });
3752
+ this.logger.info(
3753
+ `[CaptionProcessingPipeline] Reparsed ${reparsedCaptions.length} ${resourceType} captions`
3754
+ );
3755
+ } else {
3756
+ this.logger.warn(
3757
+ `[CaptionProcessingPipeline] ${failedIndices.length} ${resourceType} captions failed validation (kept as-is, fallback retry disabled)`
3758
+ );
3759
+ }
3760
+ }
3761
+ }
3762
+ return captionsByIndex;
3763
+ }
3764
+ /**
3765
+ * Extract caption text from resource
3766
+ *
3767
+ * Handles both string references and $ref resolution
3768
+ */
3769
+ extractCaptionText(captions) {
3770
+ if (!captions?.[0]) {
3771
+ return void 0;
3772
+ }
3773
+ const captionRef = captions[0];
3774
+ if (typeof captionRef === "string") {
3775
+ return captionRef;
3776
+ }
3777
+ if (this.refResolver && "$ref" in captionRef) {
3778
+ const resolved = this.refResolver.resolveText(captionRef.$ref);
3779
+ return resolved?.text;
3780
+ }
3781
+ return void 0;
3782
+ }
3783
+ };
3784
+
3785
+ // src/pipelines/toc-extraction-pipeline.ts
3786
+ var TocExtractionPipeline = class {
3787
+ logger;
3788
+ tocFinder;
3789
+ tocExtractor;
3790
+ tocContentValidator;
3791
+ visionTocExtractor;
3792
+ refResolver;
3793
+ usageAggregator;
3794
+ constructor(deps) {
3795
+ this.logger = deps.logger;
3796
+ this.tocFinder = deps.tocFinder;
3797
+ this.tocExtractor = deps.tocExtractor;
3798
+ this.tocContentValidator = deps.tocContentValidator;
3799
+ this.visionTocExtractor = deps.visionTocExtractor;
3800
+ this.refResolver = deps.refResolver;
3801
+ this.usageAggregator = deps.usageAggregator;
3802
+ }
3803
+ /**
3804
+ * Execute the TOC extraction pipeline
3805
+ *
3806
+ * @param doclingDoc - Document containing texts, pages, etc.
3807
+ * @param _filteredTexts - Pre-filtered texts (reserved for future use)
3808
+ * @returns Extracted TOC entries
3809
+ * @throws {TocNotFoundError} When TOC cannot be found or extracted
3810
+ */
3811
+ async extract(doclingDoc, _filteredTexts) {
3812
+ this.logger.info("[TocExtractionPipeline] Extracting TOC...");
3813
+ let markdown = null;
3814
+ try {
3815
+ const tocArea = this.tocFinder.find(doclingDoc);
3816
+ this.logger.info(
3817
+ `[TocExtractionPipeline] Found TOC area: pages ${tocArea.startPage}-${tocArea.endPage}`
3818
+ );
3819
+ markdown = MarkdownConverter.convert(tocArea.itemRefs, this.refResolver);
3820
+ this.logger.info(
3821
+ `[TocExtractionPipeline] Converted TOC to Markdown (${markdown.length} chars)`
3822
+ );
3823
+ const validation = await this.tocContentValidator.validate(markdown);
3824
+ if (!this.tocContentValidator.isValid(validation)) {
3825
+ this.logger.warn(
3826
+ `[TocExtractionPipeline] TOC validation failed: ${validation.reason}`
3827
+ );
3828
+ markdown = null;
3829
+ } else {
3830
+ const validMarkdown = this.tocContentValidator.getValidMarkdown(validation);
3831
+ if (validMarkdown) {
3832
+ if (validation.contentType === "mixed") {
3833
+ this.logger.info(
3834
+ `[TocExtractionPipeline] Mixed TOC detected, using extracted main TOC (${validMarkdown.length} chars)`
3835
+ );
3836
+ }
3837
+ markdown = validMarkdown;
3838
+ this.logger.info(
3839
+ `[TocExtractionPipeline] TOC validation passed (confidence: ${validation.confidence})`
3840
+ );
3841
+ } else {
3842
+ markdown = null;
3843
+ }
3844
+ }
3845
+ } catch (error) {
3846
+ if (error instanceof TocNotFoundError) {
3847
+ this.logger.info(
3848
+ "[TocExtractionPipeline] Rule-based TOC not found, will try vision fallback"
3849
+ );
3850
+ } else {
3851
+ throw error;
3852
+ }
3853
+ }
3854
+ let fromVision = false;
3855
+ const totalPages = Object.keys(doclingDoc.pages).length;
3856
+ if (!markdown) {
3857
+ fromVision = true;
3858
+ this.logger.info("[TocExtractionPipeline] Using vision fallback for TOC");
3859
+ markdown = await this.visionTocExtractor.extract(totalPages);
3860
+ if (!markdown) {
3861
+ const reason = "Both rule-based search and vision fallback failed to locate TOC";
3862
+ this.logger.error(
3863
+ `[TocExtractionPipeline] TOC extraction failed: ${reason}`
3864
+ );
3865
+ throw new TocNotFoundError(
3866
+ `Table of contents not found in the document. ${reason}.`
3867
+ );
3868
+ }
3869
+ this.logger.info(
3870
+ `[TocExtractionPipeline] Vision extracted TOC markdown (${markdown.length} chars)`
3871
+ );
3872
+ }
3873
+ const maxTocPageNo = extractMaxPageNumber(markdown);
3874
+ const effectiveTotalPages = maxTocPageNo > totalPages ? void 0 : totalPages;
3875
+ let tocResult;
3876
+ try {
3877
+ tocResult = await this.tocExtractor.extract(markdown, {
3878
+ totalPages: effectiveTotalPages
3879
+ });
3880
+ } catch (error) {
3881
+ if (error instanceof TocValidationError) {
3882
+ this.logger.warn(
3883
+ `[TocExtractionPipeline] TOC extraction validation failed: ${error.message}`
3884
+ );
3885
+ tocResult = { entries: [], usages: [] };
3886
+ } else {
3887
+ throw error;
3888
+ }
3889
+ }
3890
+ tocResult.usages.forEach((usage) => this.usageAggregator.track(usage));
3891
+ if (tocResult.entries.length === 0 && !fromVision) {
3892
+ this.logger.warn(
3893
+ "[TocExtractionPipeline] Text-based TOC extraction yielded 0 entries, retrying with vision"
3894
+ );
3895
+ const visionMarkdown = await this.visionTocExtractor.extract(totalPages);
3896
+ if (visionMarkdown) {
3897
+ this.logger.info(
3898
+ `[TocExtractionPipeline] Vision extracted TOC markdown (${visionMarkdown.length} chars)`
3899
+ );
3900
+ const visionMaxPageNo = extractMaxPageNumber(visionMarkdown);
3901
+ const visionEffectivePages = visionMaxPageNo > totalPages ? void 0 : totalPages;
3902
+ try {
3903
+ const visionResult = await this.tocExtractor.extract(visionMarkdown, {
3904
+ totalPages: visionEffectivePages
3905
+ });
3906
+ visionResult.usages.forEach(
3907
+ (usage) => this.usageAggregator.track(usage)
3908
+ );
3909
+ if (visionResult.entries.length > 0) {
3910
+ tocResult = visionResult;
3911
+ }
3912
+ } catch {
3913
+ }
3914
+ }
3915
+ }
3916
+ if (tocResult.entries.length === 0) {
3917
+ const reason = "TOC area was detected but LLM could not extract any structured entries";
3918
+ this.logger.error(
3919
+ `[TocExtractionPipeline] TOC extraction failed: ${reason}`
3920
+ );
3921
+ throw new TocNotFoundError(`${reason}.`);
3922
+ }
3923
+ this.logger.info(
3924
+ `[TocExtractionPipeline] Extracted ${tocResult.entries.length} top-level TOC entries`
3925
+ );
3926
+ return tocResult.entries;
3927
+ }
3928
+ };
3929
+
3930
+ // src/validators/base-validator.ts
3931
+ var BaseValidator = class extends TextLLMComponent {
3932
+ /**
3933
+ * Validator name for logging (kept for backwards compatibility)
3934
+ */
3935
+ validatorName;
3936
+ /**
3937
+ * Constructor for BaseValidator
3938
+ *
3939
+ * @param logger - Logger instance
3940
+ * @param model - Language model to use for validation
3941
+ * @param validatorName - Name of the validator for logging (e.g., "TocContentValidator")
3942
+ * @param options - Optional configuration (maxRetries, temperature)
3943
+ * @param fallbackModel - Optional fallback model for retry on failure
3944
+ * @param aggregator - Optional token usage aggregator for tracking LLM calls
3945
+ */
3946
+ constructor(logger, model, validatorName, options, fallbackModel, aggregator) {
3947
+ super(logger, model, validatorName, options, fallbackModel, aggregator);
3948
+ this.validatorName = validatorName;
3949
+ }
3950
+ /**
3951
+ * Call LLM with LLMCaller
3952
+ *
3953
+ * This method provides backwards compatibility with existing validators.
3954
+ * It wraps the parent callTextLLM method but allows passing a custom aggregator.
3955
+ *
3956
+ * @param schema - Zod schema for response validation
3957
+ * @param systemPrompt - System prompt
3958
+ * @param userPrompt - User prompt
3959
+ * @param phase - Phase name for tracking (e.g., 'validation', 'batch-validation')
3960
+ * @param aggregator - Optional token usage aggregator for tracking this call
3961
+ * @returns Parsed and validated LLM response with usage information
3962
+ */
3963
+ async callLLM(schema, systemPrompt, userPrompt, phase, aggregator) {
3964
+ const result = await LLMCaller.call({
3965
+ schema,
3613
3966
  systemPrompt,
3614
3967
  userPrompt,
3615
3968
  primaryModel: this.model,
@@ -4022,7 +4375,9 @@ var DocumentProcessor = class {
4022
4375
  visionTocExtractor;
4023
4376
  captionParser;
4024
4377
  chapterConverter;
4025
- textCleaner = TextCleaner;
4378
+ resourceConverter;
4379
+ tocExtractionPipeline;
4380
+ captionProcessingPipeline;
4026
4381
  usageAggregator = new LLMTokenUsageAggregator();
4027
4382
  constructor(options) {
4028
4383
  this.logger = options.logger;
@@ -4105,16 +4460,16 @@ var DocumentProcessor = class {
4105
4460
  this.emitTokenUsage();
4106
4461
  this.checkAborted();
4107
4462
  const startTimeToc = Date.now();
4108
- const tocEntries = await this.extractTableOfContents(doclingDoc, filtered);
4463
+ const tocEntries = await this.tocExtractionPipeline.extract(
4464
+ doclingDoc,
4465
+ filtered
4466
+ );
4109
4467
  const tocTime = Date.now() - startTimeToc;
4110
4468
  this.logger.info(`[DocumentProcessor] TOC extraction took ${tocTime}ms`);
4111
4469
  this.emitTokenUsage();
4112
4470
  this.checkAborted();
4113
4471
  const startTimeResources = Date.now();
4114
- const { images, tables, footnotes } = await this.convertResources(
4115
- doclingDoc,
4116
- outputPath
4117
- );
4472
+ const { images, tables, footnotes } = await this.resourceConverter.convertAll(doclingDoc, outputPath);
4118
4473
  const resourcesTime = Date.now() - startTimeResources;
4119
4474
  this.logger.info(
4120
4475
  `[DocumentProcessor] Resource conversion took ${resourcesTime}ms`
@@ -4219,7 +4574,37 @@ var DocumentProcessor = class {
4219
4574
  );
4220
4575
  this.logger.info("[DocumentProcessor] - ChapterConverter");
4221
4576
  this.chapterConverter = new ChapterConverter(this.logger, this.idGenerator);
4222
- this.logger.info("[DocumentProcessor] All processors initialized");
4577
+ this.logger.info("[DocumentProcessor] - TocExtractionPipeline");
4578
+ this.tocExtractionPipeline = new TocExtractionPipeline({
4579
+ logger: this.logger,
4580
+ tocFinder: this.tocFinder,
4581
+ tocExtractor: this.tocExtractor,
4582
+ tocContentValidator: this.tocContentValidator,
4583
+ visionTocExtractor: this.visionTocExtractor,
4584
+ refResolver: this.refResolver,
4585
+ usageAggregator: this.usageAggregator
4586
+ });
4587
+ this.logger.info("[DocumentProcessor] - CaptionProcessingPipeline");
4588
+ this.captionProcessingPipeline = new CaptionProcessingPipeline({
4589
+ logger: this.logger,
4590
+ captionParser: this.captionParser,
4591
+ captionValidator: this.captionValidator,
4592
+ refResolver: this.refResolver,
4593
+ fallbackModel: this.fallbackModel,
4594
+ enableFallbackRetry: this.enableFallbackRetry,
4595
+ maxRetries: this.maxRetries,
4596
+ captionParserBatchSize: this.captionParserBatchSize,
4597
+ captionValidatorBatchSize: this.captionValidatorBatchSize,
4598
+ usageAggregator: this.usageAggregator,
4599
+ abortSignal: this.abortSignal
4600
+ });
4601
+ this.logger.info("[DocumentProcessor] - ResourceConverter");
4602
+ this.resourceConverter = new ResourceConverter(
4603
+ this.logger,
4604
+ this.idGenerator,
4605
+ this.captionProcessingPipeline
4606
+ );
4607
+ this.logger.info("[DocumentProcessor] All processors initialized");
4223
4608
  }
4224
4609
  /**
4225
4610
  * Normalize and filter texts using TextCleaner
@@ -4230,7 +4615,7 @@ var DocumentProcessor = class {
4230
4615
  normalizeAndFilterTexts(doclingDoc) {
4231
4616
  this.logger.info("[DocumentProcessor] Normalizing and filtering texts...");
4232
4617
  const texts = doclingDoc.texts.map((text) => text.text);
4233
- const filtered = this.textCleaner.normalizeAndFilterBatch(
4618
+ const filtered = TextCleaner.normalizeAndFilterBatch(
4234
4619
  texts,
4235
4620
  this.textCleanerBatchSize
4236
4621
  );
@@ -4254,58 +4639,6 @@ var DocumentProcessor = class {
4254
4639
  );
4255
4640
  return pageRangeMap;
4256
4641
  }
4257
- /**
4258
- * Convert images, tables, and footnotes
4259
- *
4260
- * Runs conversions:
4261
- * - Images conversion (with caption extraction)
4262
- * - Tables conversion (with caption extraction, excluding TOC tables)
4263
- * - Footnotes conversion (synchronous, from text items with label='footnote')
4264
- */
4265
- async convertResources(doclingDoc, outputPath) {
4266
- this.logger.info(
4267
- "[DocumentProcessor] Converting images, tables, and footnotes..."
4268
- );
4269
- const [images, tables] = await Promise.all([
4270
- this.convertImages(doclingDoc, outputPath),
4271
- this.convertTables(doclingDoc)
4272
- ]);
4273
- const footnotes = this.convertFootnotes(doclingDoc);
4274
- this.logger.info(
4275
- `[DocumentProcessor] Converted ${images.length} images, ${tables.length} tables, and ${footnotes.length} footnotes`
4276
- );
4277
- return { images, tables, footnotes };
4278
- }
4279
- /**
4280
- * Convert footnotes
4281
- *
4282
- * Extracts footnotes from DoclingDocument text items with label='footnote'
4283
- */
4284
- convertFootnotes(doclingDoc) {
4285
- const footnoteItems = doclingDoc.texts.filter(
4286
- (item) => item.label === "footnote"
4287
- );
4288
- this.logger.info(
4289
- `[DocumentProcessor] Converting ${footnoteItems.length} footnotes...`
4290
- );
4291
- const footnotes = [];
4292
- for (const item of footnoteItems) {
4293
- if (!this.textCleaner.isValidText(item.text)) {
4294
- continue;
4295
- }
4296
- const pdfPageNo = item.prov?.[0]?.page_no ?? 1;
4297
- const footnoteId = this.idGenerator.generateFootnoteId();
4298
- footnotes.push({
4299
- id: footnoteId,
4300
- text: this.textCleaner.normalize(item.text),
4301
- pdfPageNo
4302
- });
4303
- }
4304
- this.logger.info(
4305
- `[DocumentProcessor] Converted ${footnotes.length} valid footnotes`
4306
- );
4307
- return footnotes;
4308
- }
4309
4642
  /**
4310
4643
  * Assemble the final ProcessedDocument
4311
4644
  *
@@ -4326,386 +4659,6 @@ var DocumentProcessor = class {
4326
4659
  );
4327
4660
  return processedDoc;
4328
4661
  }
4329
- /**
4330
- * Extract table of contents (TOC)
4331
- *
4332
- * Uses rule-based extraction with LLM validation and vision fallback:
4333
- * 1. TocFinder - find TOC area in document (rule-based)
4334
- * 2. MarkdownConverter - convert TOC items to Markdown
4335
- * 3. TocContentValidator - validate if content is actually a TOC (LLM)
4336
- * 4. If invalid: VisionTocExtractor - extract from page images (vision LLM fallback)
4337
- * 5. TocExtractor - LLM-based structured extraction
4338
- */
4339
- async extractTableOfContents(doclingDoc, _filteredTexts) {
4340
- this.logger.info("[DocumentProcessor] Extracting TOC...");
4341
- let markdown = null;
4342
- try {
4343
- const tocArea = this.tocFinder.find(doclingDoc);
4344
- this.logger.info(
4345
- `[DocumentProcessor] Found TOC area: pages ${tocArea.startPage}-${tocArea.endPage}`
4346
- );
4347
- markdown = MarkdownConverter.convert(tocArea.itemRefs, this.refResolver);
4348
- this.logger.info(
4349
- `[DocumentProcessor] Converted TOC to Markdown (${markdown.length} chars)`
4350
- );
4351
- const validation = await this.tocContentValidator.validate(markdown);
4352
- if (!this.tocContentValidator.isValid(validation)) {
4353
- this.logger.warn(
4354
- `[DocumentProcessor] TOC validation failed: ${validation.reason}`
4355
- );
4356
- markdown = null;
4357
- } else {
4358
- const validMarkdown = this.tocContentValidator.getValidMarkdown(validation);
4359
- if (validMarkdown) {
4360
- if (validation.contentType === "mixed") {
4361
- this.logger.info(
4362
- `[DocumentProcessor] Mixed TOC detected, using extracted main TOC (${validMarkdown.length} chars)`
4363
- );
4364
- }
4365
- markdown = validMarkdown;
4366
- this.logger.info(
4367
- `[DocumentProcessor] TOC validation passed (confidence: ${validation.confidence})`
4368
- );
4369
- } else {
4370
- markdown = null;
4371
- }
4372
- }
4373
- } catch (error) {
4374
- if (error instanceof TocNotFoundError) {
4375
- this.logger.info(
4376
- "[DocumentProcessor] Rule-based TOC not found, will try vision fallback"
4377
- );
4378
- } else {
4379
- throw error;
4380
- }
4381
- }
4382
- let fromVision = false;
4383
- if (!markdown) {
4384
- fromVision = true;
4385
- this.logger.info("[DocumentProcessor] Using vision fallback for TOC");
4386
- const totalPages2 = Object.keys(doclingDoc.pages).length;
4387
- markdown = await this.visionTocExtractor.extract(totalPages2);
4388
- if (!markdown) {
4389
- const reason = "Both rule-based search and vision fallback failed to locate TOC";
4390
- this.logger.error(
4391
- `[DocumentProcessor] TOC extraction failed: ${reason}`
4392
- );
4393
- throw new TocNotFoundError(
4394
- `Table of contents not found in the document. ${reason}.`
4395
- );
4396
- }
4397
- this.logger.info(
4398
- `[DocumentProcessor] Vision extracted TOC markdown (${markdown.length} chars)`
4399
- );
4400
- }
4401
- const totalPages = Object.keys(doclingDoc.pages).length;
4402
- const maxTocPageNo = this.extractMaxPageNumber(markdown);
4403
- const effectiveTotalPages = maxTocPageNo > totalPages ? void 0 : totalPages;
4404
- let tocResult;
4405
- try {
4406
- tocResult = await this.tocExtractor.extract(markdown, {
4407
- totalPages: effectiveTotalPages
4408
- });
4409
- } catch (error) {
4410
- if (error instanceof TocValidationError) {
4411
- this.logger.warn(
4412
- `[DocumentProcessor] TOC extraction validation failed: ${error.message}`
4413
- );
4414
- tocResult = { entries: [], usages: [] };
4415
- } else {
4416
- throw error;
4417
- }
4418
- }
4419
- for (const usage of tocResult.usages) {
4420
- this.usageAggregator.track(usage);
4421
- }
4422
- if (tocResult.entries.length === 0 && !fromVision) {
4423
- this.logger.warn(
4424
- "[DocumentProcessor] Text-based TOC extraction yielded 0 entries, retrying with vision"
4425
- );
4426
- const visionMarkdown = await this.visionTocExtractor.extract(totalPages);
4427
- if (visionMarkdown) {
4428
- this.logger.info(
4429
- `[DocumentProcessor] Vision extracted TOC markdown (${visionMarkdown.length} chars)`
4430
- );
4431
- const visionMaxPageNo = this.extractMaxPageNumber(visionMarkdown);
4432
- const visionEffectivePages = visionMaxPageNo > totalPages ? void 0 : totalPages;
4433
- try {
4434
- const visionResult = await this.tocExtractor.extract(
4435
- visionMarkdown,
4436
- {
4437
- totalPages: visionEffectivePages
4438
- }
4439
- );
4440
- for (const usage of visionResult.usages) {
4441
- this.usageAggregator.track(usage);
4442
- }
4443
- if (visionResult.entries.length > 0) {
4444
- tocResult = visionResult;
4445
- }
4446
- } catch {
4447
- }
4448
- }
4449
- }
4450
- if (tocResult.entries.length === 0) {
4451
- const reason = "TOC area was detected but LLM could not extract any structured entries";
4452
- this.logger.error(`[DocumentProcessor] TOC extraction failed: ${reason}`);
4453
- throw new TocNotFoundError(`${reason}.`);
4454
- }
4455
- this.logger.info(
4456
- `[DocumentProcessor] Extracted ${tocResult.entries.length} top-level TOC entries`
4457
- );
4458
- return tocResult.entries;
4459
- }
4460
- /**
4461
- * Extract the maximum page number from TOC markdown
4462
- *
4463
- * Parses page numbers from dot-leader patterns (e.g., "..... 175")
4464
- * and table cell patterns (e.g., "| title | 175 |") to detect compiled
4465
- * volume scenarios where TOC page numbers exceed the sub-document's page count.
4466
- */
4467
- extractMaxPageNumber(markdown) {
4468
- const dotLeaderMatches = [...markdown.matchAll(/\.{2,}\s*(\d+)/g)];
4469
- const tableCellMatches = [...markdown.matchAll(/\|\s*(\d+)\s*\|\s*$/gm)];
4470
- const allNumbers = [
4471
- ...dotLeaderMatches.map((m) => parseInt(m[1], 10)),
4472
- ...tableCellMatches.map((m) => parseInt(m[1], 10))
4473
- ];
4474
- if (allNumbers.length === 0) {
4475
- return 0;
4476
- }
4477
- return Math.max(...allNumbers);
4478
- }
4479
- /**
4480
- * Process resource captions (for images and tables)
4481
- *
4482
- * Common caption processing pipeline:
4483
- * 1. Parse captions in batch
4484
- * 2. Validate parsed captions
4485
- * 3. Reparse failed captions with fallback model
4486
- *
4487
- * @param captionTexts - Array of caption texts to process
4488
- * @param resourceType - Type of resource for logging (e.g., 'image', 'table')
4489
- * @returns Parsed captions with index mapping
4490
- */
4491
- async processResourceCaptions(captionTexts, resourceType) {
4492
- const captionsByIndex = /* @__PURE__ */ new Map();
4493
- const validCaptionData = [];
4494
- for (let i = 0; i < captionTexts.length; i++) {
4495
- const text = captionTexts[i];
4496
- if (text !== void 0) {
4497
- validCaptionData.push({
4498
- resourceIndex: i,
4499
- filteredIndex: validCaptionData.length,
4500
- text
4501
- });
4502
- }
4503
- }
4504
- const validCaptionTexts = validCaptionData.map((item) => item.text);
4505
- const parsedCaptions = validCaptionTexts.length > 0 ? await this.captionParser.parseBatch(
4506
- validCaptionTexts,
4507
- this.captionParserBatchSize
4508
- ) : [];
4509
- let finalValidCaptionData = validCaptionData;
4510
- let finalParsedCaptions = parsedCaptions;
4511
- if (parsedCaptions.length !== validCaptionData.length) {
4512
- this.logger.warn(
4513
- `[DocumentProcessor] Caption parsing length mismatch for ${resourceType}: expected ${validCaptionData.length}, got ${parsedCaptions.length}. Attempting recovery by matching fullText...`
4514
- );
4515
- const parsedMap = /* @__PURE__ */ new Map();
4516
- for (const parsed of parsedCaptions) {
4517
- parsedMap.set(parsed.fullText, parsed);
4518
- }
4519
- const recoveredData = [];
4520
- for (const item of validCaptionData) {
4521
- if (parsedMap.has(item.text)) {
4522
- recoveredData.push(item);
4523
- } else {
4524
- this.logger.warn(
4525
- `[DocumentProcessor] Skipping ${resourceType} caption at index ${item.resourceIndex}: "${item.text}" (not found in parsed results)`
4526
- );
4527
- }
4528
- }
4529
- const recoveredCaptions = [];
4530
- for (const item of recoveredData) {
4531
- const caption = parsedMap.get(item.text);
4532
- if (caption) {
4533
- recoveredCaptions.push(caption);
4534
- }
4535
- }
4536
- if (recoveredCaptions.length !== recoveredData.length) {
4537
- throw new Error(
4538
- `[DocumentProcessor] Failed to recover from length mismatch: recovered ${recoveredCaptions.length} captions for ${recoveredData.length} valid items`
4539
- );
4540
- }
4541
- finalValidCaptionData = recoveredData;
4542
- finalParsedCaptions = recoveredCaptions;
4543
- this.logger.info(
4544
- `[DocumentProcessor] Successfully recovered ${finalParsedCaptions.length} ${resourceType} captions after length mismatch`
4545
- );
4546
- }
4547
- for (let i = 0; i < finalParsedCaptions.length; i++) {
4548
- const resourceIndex = finalValidCaptionData[i].resourceIndex;
4549
- captionsByIndex.set(resourceIndex, finalParsedCaptions[i]);
4550
- }
4551
- if (finalParsedCaptions.length > 0) {
4552
- const finalValidCaptionTexts = finalValidCaptionData.map(
4553
- (item) => item.text
4554
- );
4555
- const validationResults = await this.captionValidator.validateBatch(
4556
- finalParsedCaptions,
4557
- finalValidCaptionTexts,
4558
- this.captionValidatorBatchSize
4559
- );
4560
- const failedIndices = validationResults.map((isValid, index) => isValid ? -1 : index).filter((index) => index !== -1);
4561
- if (failedIndices.length > 0) {
4562
- for (const filteredIndex of failedIndices) {
4563
- const captionData = finalValidCaptionData[filteredIndex];
4564
- const originalText = captionData.text;
4565
- const parsedNum = finalParsedCaptions[filteredIndex].num;
4566
- const resourceIndex = captionData.resourceIndex;
4567
- this.logger.warn(
4568
- `[DocumentProcessor] Invalid ${resourceType} caption [${resourceIndex}]: "${originalText}" | parsed num="${parsedNum}"`
4569
- );
4570
- }
4571
- if (this.enableFallbackRetry) {
4572
- this.logger.info(
4573
- `[DocumentProcessor] Reparsing ${failedIndices.length} failed ${resourceType} captions with fallback model...`
4574
- );
4575
- const failedCaptionTexts = failedIndices.map(
4576
- (filteredIndex) => finalValidCaptionData[filteredIndex].text
4577
- );
4578
- const fallbackCaptionParser = new CaptionParser(
4579
- this.logger,
4580
- this.fallbackModel,
4581
- {
4582
- maxRetries: this.maxRetries,
4583
- componentName: "CaptionParser-fallback",
4584
- abortSignal: this.abortSignal
4585
- },
4586
- void 0,
4587
- // no fallback for the fallback
4588
- this.usageAggregator
4589
- );
4590
- const reparsedCaptions = await fallbackCaptionParser.parseBatch(
4591
- failedCaptionTexts,
4592
- 0
4593
- // sequential processing
4594
- );
4595
- for (let i = 0; i < failedIndices.length; i++) {
4596
- const filteredIndex = failedIndices[i];
4597
- const resourceIndex = finalValidCaptionData[filteredIndex].resourceIndex;
4598
- captionsByIndex.set(resourceIndex, reparsedCaptions[i]);
4599
- }
4600
- this.logger.info(
4601
- `[DocumentProcessor] Reparsed ${reparsedCaptions.length} ${resourceType} captions`
4602
- );
4603
- } else {
4604
- this.logger.warn(
4605
- `[DocumentProcessor] ${failedIndices.length} ${resourceType} captions failed validation (kept as-is, fallback retry disabled)`
4606
- );
4607
- }
4608
- }
4609
- }
4610
- return captionsByIndex;
4611
- }
4612
- /**
4613
- * Extract caption text from resource
4614
- *
4615
- * Handles both string references and $ref resolution
4616
- */
4617
- extractCaptionText(captions) {
4618
- if (!captions?.[0]) {
4619
- return void 0;
4620
- }
4621
- const captionRef = captions[0];
4622
- if (typeof captionRef === "string") {
4623
- return captionRef;
4624
- }
4625
- if (this.refResolver && "$ref" in captionRef) {
4626
- const resolved = this.refResolver.resolveText(captionRef.$ref);
4627
- return resolved?.text;
4628
- }
4629
- return void 0;
4630
- }
4631
- /**
4632
- * Convert images
4633
- *
4634
- * Converts pictures from DoclingDocument to ProcessedImage
4635
- */
4636
- async convertImages(doclingDoc, outputPath) {
4637
- this.logger.info(
4638
- `[DocumentProcessor] Converting ${doclingDoc.pictures.length} images...`
4639
- );
4640
- const images = [];
4641
- const captionTexts = [];
4642
- for (const picture of doclingDoc.pictures) {
4643
- const pdfPageNo = picture.prov?.[0]?.page_no ?? 0;
4644
- const imageId = this.idGenerator?.generateImageId() ?? `img-${images.length + 1}`;
4645
- const captionText = this.extractCaptionText(picture.captions);
4646
- captionTexts.push(captionText);
4647
- images.push({
4648
- id: imageId,
4649
- path: `${outputPath}/images/image_${images.length}.png`,
4650
- pdfPageNo
4651
- // caption will be assigned later
4652
- });
4653
- }
4654
- const captionsByIndex = await this.processResourceCaptions(
4655
- captionTexts,
4656
- "image"
4657
- );
4658
- for (let i = 0; i < images.length; i++) {
4659
- if (captionsByIndex.has(i)) {
4660
- images[i].caption = captionsByIndex.get(i);
4661
- }
4662
- }
4663
- return images;
4664
- }
4665
- /**
4666
- * Convert tables
4667
- *
4668
- * Converts tables from DoclingDocument to ProcessedTable
4669
- */
4670
- async convertTables(doclingDoc) {
4671
- this.logger.info(
4672
- `[DocumentProcessor] Converting ${doclingDoc.tables.length} tables...`
4673
- );
4674
- const tables = [];
4675
- const captionTexts = [];
4676
- for (const table of doclingDoc.tables) {
4677
- const pdfPageNo = table.prov?.[0]?.page_no ?? 0;
4678
- const tableId = this.idGenerator?.generateTableId() ?? `tbl-${tables.length + 1}`;
4679
- const grid = table.data.grid.map(
4680
- (row) => row.map((cell) => ({
4681
- text: cell.text,
4682
- rowSpan: cell.row_span ?? 1,
4683
- colSpan: cell.col_span ?? 1,
4684
- isHeader: cell.column_header || cell.row_header || false
4685
- }))
4686
- );
4687
- const captionText = this.extractCaptionText(table.captions);
4688
- captionTexts.push(captionText);
4689
- tables.push({
4690
- id: tableId,
4691
- pdfPageNo,
4692
- numRows: grid.length,
4693
- numCols: grid[0]?.length ?? 0,
4694
- grid
4695
- // caption will be assigned later
4696
- });
4697
- }
4698
- const captionsByIndex = await this.processResourceCaptions(
4699
- captionTexts,
4700
- "table"
4701
- );
4702
- for (let i = 0; i < tables.length; i++) {
4703
- if (captionsByIndex.has(i)) {
4704
- tables[i].caption = captionsByIndex.get(i);
4705
- }
4706
- }
4707
- return tables;
4708
- }
4709
4662
  /**
4710
4663
  * Convert chapters and link resources
4711
4664
  *
@@ -4739,6 +4692,7 @@ export {
4739
4692
  CONTINUATION_MARKERS,
4740
4693
  CaptionParseError,
4741
4694
  CaptionParser,
4695
+ CaptionProcessingPipeline,
4742
4696
  CaptionValidationError,
4743
4697
  CaptionValidator,
4744
4698
  ChapterConverter,
@@ -4747,12 +4701,14 @@ export {
4747
4701
  PagePattern,
4748
4702
  PageRangeParseError,
4749
4703
  PageRangeParser,
4704
+ ResourceConverter,
4750
4705
  TOC_KEYWORDS,
4751
4706
  TextLLMComponent,
4752
4707
  TocContentValidationSchema,
4753
4708
  TocContentValidator,
4754
4709
  TocEntrySchema,
4755
4710
  TocExtractError,
4711
+ TocExtractionPipeline,
4756
4712
  TocExtractor,
4757
4713
  TocFinder,
4758
4714
  TocNotFoundError,
@@ -4760,6 +4716,7 @@ export {
4760
4716
  TocResponseSchema,
4761
4717
  VisionLLMComponent,
4762
4718
  VisionTocExtractionSchema,
4763
- VisionTocExtractor
4719
+ VisionTocExtractor,
4720
+ extractMaxPageNumber
4764
4721
  };
4765
4722
  //# sourceMappingURL=index.js.map