@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.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
833
|
+
return _MarkdownConverter.groupToMarkdown(
|
|
866
834
|
child,
|
|
867
835
|
refResolver,
|
|
868
836
|
indentLevel + 1
|
|
869
837
|
);
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
|
|
866
|
+
return [];
|
|
905
867
|
}
|
|
906
868
|
const cells = row.map(
|
|
907
869
|
(cell) => _MarkdownConverter.escapeTableCell(cell.text)
|
|
908
870
|
);
|
|
909
|
-
|
|
871
|
+
const rowLine = `| ${cells.join(" | ")} |`;
|
|
910
872
|
if (rowIdx === 0) {
|
|
911
873
|
const separator = row.map(() => "---").join(" | ");
|
|
912
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
if (
|
|
1826
|
-
const
|
|
1827
|
-
if (
|
|
1828
|
-
|
|
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
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
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
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
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
|
-
|
|
3287
|
-
|
|
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/
|
|
3578
|
-
var
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
this.
|
|
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
|
-
*
|
|
3642
|
+
* Process resource captions (for images and tables)
|
|
3599
3643
|
*
|
|
3600
|
-
*
|
|
3601
|
-
*
|
|
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
|
|
3604
|
-
* @param
|
|
3605
|
-
* @
|
|
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
|
|
3611
|
-
const
|
|
3612
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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]
|
|
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 =
|
|
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
|