@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/README.ko.md +28 -16
- package/README.md +28 -16
- package/dist/index.cjs +649 -688
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +156 -69
- package/dist/index.d.ts +156 -69
- package/dist/index.js +644 -687
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
893
|
+
return _MarkdownConverter.groupToMarkdown(
|
|
922
894
|
child,
|
|
923
895
|
refResolver,
|
|
924
896
|
indentLevel + 1
|
|
925
897
|
);
|
|
926
|
-
|
|
927
|
-
|
|
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
|
-
|
|
926
|
+
return [];
|
|
961
927
|
}
|
|
962
928
|
const cells = row.map(
|
|
963
929
|
(cell) => _MarkdownConverter.escapeTableCell(cell.text)
|
|
964
930
|
);
|
|
965
|
-
|
|
931
|
+
const rowLine = `| ${cells.join(" | ")} |`;
|
|
966
932
|
if (rowIdx === 0) {
|
|
967
933
|
const separator = row.map(() => "---").join(" | ");
|
|
968
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
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
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
if (
|
|
1882
|
-
const
|
|
1883
|
-
if (
|
|
1884
|
-
|
|
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
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
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
|
-
|
|
3343
|
-
|
|
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/
|
|
3634
|
-
var
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
this.
|
|
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
|
-
*
|
|
3702
|
+
* Process resource captions (for images and tables)
|
|
3655
3703
|
*
|
|
3656
|
-
*
|
|
3657
|
-
*
|
|
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
|
|
3660
|
-
* @param
|
|
3661
|
-
* @
|
|
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
|
|
3667
|
-
const
|
|
3668
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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]
|
|
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 =
|
|
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
|