@heripo/pdf-parser 0.1.30 → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +587 -284
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +519 -216
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -40,7 +40,7 @@ module.exports = __toCommonJS(src_exports);
|
|
|
40
40
|
|
|
41
41
|
// src/core/pdf-parser.ts
|
|
42
42
|
var import_docling_sdk = require("docling-sdk");
|
|
43
|
-
var
|
|
43
|
+
var import_node_path18 = require("path");
|
|
44
44
|
|
|
45
45
|
// src/config/constants.ts
|
|
46
46
|
var PDF_PARSER = {
|
|
@@ -84,7 +84,7 @@ var DOCLING_ENVIRONMENT = {
|
|
|
84
84
|
var PAGE_RENDERING = {
|
|
85
85
|
/** Default rendering DPI for VLM text recognition quality */
|
|
86
86
|
DEFAULT_DPI: 200,
|
|
87
|
-
/** Low-resolution DPI for
|
|
87
|
+
/** Low-resolution DPI for language detection sampling */
|
|
88
88
|
SAMPLE_DPI: 150
|
|
89
89
|
};
|
|
90
90
|
var CHUNKED_CONVERSION = {
|
|
@@ -1015,17 +1015,14 @@ function checkMacOSVersion() {
|
|
|
1015
1015
|
}
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
|
-
// src/
|
|
1019
|
-
var
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
}
|
|
1027
|
-
name = "ImagePdfFallbackError";
|
|
1028
|
-
};
|
|
1018
|
+
// src/core/pdf-converter.ts
|
|
1019
|
+
var import_node_fs17 = require("fs");
|
|
1020
|
+
var import_node_path17 = require("path");
|
|
1021
|
+
|
|
1022
|
+
// src/detectors/pdf-language-detector.ts
|
|
1023
|
+
var import_model = require("@heripo/model");
|
|
1024
|
+
var import_node_fs = require("fs");
|
|
1025
|
+
var import_v4 = require("zod/v4");
|
|
1029
1026
|
|
|
1030
1027
|
// src/processors/pdf-text-extractor.ts
|
|
1031
1028
|
var PdfTextExtractor = class _PdfTextExtractor {
|
|
@@ -1156,6 +1153,374 @@ var PdfTextExtractor = class _PdfTextExtractor {
|
|
|
1156
1153
|
}
|
|
1157
1154
|
};
|
|
1158
1155
|
|
|
1156
|
+
// src/prompts/korean-document-detection-prompt.ts
|
|
1157
|
+
var KOREAN_DOCUMENT_DETECTION_PROMPT = `Look at this page image carefully and identify all languages present on this page.
|
|
1158
|
+
|
|
1159
|
+
Return an array of ocrmac-compatible language tags ordered by prevalence (primary language first).
|
|
1160
|
+
Use ko-KR when Korean text is present, even when the page also contains Hanja (\u6F22\u5B57/Chinese characters), English, numbers, tables, or captions.
|
|
1161
|
+
|
|
1162
|
+
Supported tags: ar-SA, ars-SA, cs-CZ, da-DK, de-DE, en-US, es-ES, fr-FR, id-ID, it-IT, ja-JP, ko-KR, ms-MY, nb-NO, nl-NL, nn-NO, no-NO, pl-PL, pt-BR, ro-RO, ru-RU, sv-SE, th-TH, tr-TR, uk-UA, vi-VT, yue-Hans, yue-Hant, zh-Hans, zh-Hant.
|
|
1163
|
+
Examples: ["ko-KR", "en-US"], ["ja-JP"], ["zh-Hant", "en-US"]`;
|
|
1164
|
+
|
|
1165
|
+
// src/detectors/pdf-language-detector.ts
|
|
1166
|
+
var DEFAULT_MAX_SAMPLE_PAGES = 15;
|
|
1167
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
1168
|
+
var EDGE_TRIM_RATIO = 0.1;
|
|
1169
|
+
var HANGUL_REGEX = /[\u1100-\u11ff\u3130-\u318f\uac00-\ud7af]/g;
|
|
1170
|
+
var JAPANESE_KANA_REGEX = /[\u3040-\u30ff]/g;
|
|
1171
|
+
var LATIN_REGEX = /[A-Za-z]/g;
|
|
1172
|
+
var DEFAULT_OCR_LANGUAGES = ["ko-KR", "en-US"];
|
|
1173
|
+
var languageDetectionSchema = import_v4.z.object({
|
|
1174
|
+
detectedLanguages: import_v4.z.array(import_v4.z.string()).describe(
|
|
1175
|
+
"BCP 47 language tags found on this page, ordered by prevalence."
|
|
1176
|
+
)
|
|
1177
|
+
});
|
|
1178
|
+
var PdfLanguageDetector = class {
|
|
1179
|
+
constructor(logger, pageRenderer, textExtractor) {
|
|
1180
|
+
this.logger = logger;
|
|
1181
|
+
this.pageRenderer = pageRenderer;
|
|
1182
|
+
this.textExtractor = textExtractor ?? new PdfTextExtractor(logger);
|
|
1183
|
+
}
|
|
1184
|
+
textExtractor;
|
|
1185
|
+
async detect(pdfPath, outputDir, options) {
|
|
1186
|
+
this.logger.info(
|
|
1187
|
+
"[PdfLanguageDetector] Starting PDF language detection..."
|
|
1188
|
+
);
|
|
1189
|
+
const textLayerResult = await this.detectFromTextLayer(pdfPath);
|
|
1190
|
+
if (textLayerResult) {
|
|
1191
|
+
return textLayerResult;
|
|
1192
|
+
}
|
|
1193
|
+
if (!options?.model) {
|
|
1194
|
+
this.logger.info(
|
|
1195
|
+
`[PdfLanguageDetector] Language detection model not configured; using default OCR languages: ${JSON.stringify(DEFAULT_OCR_LANGUAGES)}`
|
|
1196
|
+
);
|
|
1197
|
+
return {
|
|
1198
|
+
detectedLanguages: [...DEFAULT_OCR_LANGUAGES],
|
|
1199
|
+
reason: "Language detection model not configured",
|
|
1200
|
+
sampledPages: 0,
|
|
1201
|
+
totalPages: 0,
|
|
1202
|
+
source: "default"
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
return this.detectFromSampledPages(pdfPath, outputDir, {
|
|
1206
|
+
...options,
|
|
1207
|
+
model: options.model
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
async detectFromTextLayer(pdfPath) {
|
|
1211
|
+
try {
|
|
1212
|
+
const totalPages = await this.textExtractor.getPageCount(pdfPath);
|
|
1213
|
+
if (totalPages === 0) return null;
|
|
1214
|
+
const fullText = await this.textExtractor.extractFullText(pdfPath);
|
|
1215
|
+
if (fullText.trim().length === 0) {
|
|
1216
|
+
this.logger.debug(
|
|
1217
|
+
"[PdfLanguageDetector] No text in text layer; falling back to sampled language detection"
|
|
1218
|
+
);
|
|
1219
|
+
return null;
|
|
1220
|
+
}
|
|
1221
|
+
const detectedLanguages = this.detectLanguagesFromText(fullText);
|
|
1222
|
+
if (detectedLanguages.length === 0) {
|
|
1223
|
+
this.logger.debug(
|
|
1224
|
+
"[PdfLanguageDetector] No known language signal in text layer; falling back to sampled language detection"
|
|
1225
|
+
);
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
this.logger.info(
|
|
1229
|
+
`[PdfLanguageDetector] Languages detected in text layer: ${detectedLanguages.join(", ")}`
|
|
1230
|
+
);
|
|
1231
|
+
return {
|
|
1232
|
+
detectedLanguages,
|
|
1233
|
+
reason: `Languages found in PDF text layer (${totalPages} pages checked)`,
|
|
1234
|
+
sampledPages: totalPages,
|
|
1235
|
+
totalPages,
|
|
1236
|
+
source: "text-layer"
|
|
1237
|
+
};
|
|
1238
|
+
} catch {
|
|
1239
|
+
this.logger.debug(
|
|
1240
|
+
"[PdfLanguageDetector] Text layer language detection failed; falling back to sampled language detection"
|
|
1241
|
+
);
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
detectLanguagesFromText(text) {
|
|
1246
|
+
const scores = [
|
|
1247
|
+
{ tag: "ko-KR", count: this.countMatches(text, HANGUL_REGEX) },
|
|
1248
|
+
{ tag: "ja-JP", count: this.countMatches(text, JAPANESE_KANA_REGEX) },
|
|
1249
|
+
{ tag: "en-US", count: this.countMatches(text, LATIN_REGEX) }
|
|
1250
|
+
];
|
|
1251
|
+
return scores.filter(({ count }) => count > 0).sort((a, b) => b.count - a.count).map(({ tag }) => tag);
|
|
1252
|
+
}
|
|
1253
|
+
countMatches(text, regex) {
|
|
1254
|
+
regex.lastIndex = 0;
|
|
1255
|
+
return text.match(regex)?.length ?? 0;
|
|
1256
|
+
}
|
|
1257
|
+
async detectFromSampledPages(pdfPath, outputDir, options) {
|
|
1258
|
+
const renderResult = await this.pageRenderer.renderPages(
|
|
1259
|
+
pdfPath,
|
|
1260
|
+
outputDir,
|
|
1261
|
+
{
|
|
1262
|
+
dpi: PAGE_RENDERING.SAMPLE_DPI
|
|
1263
|
+
}
|
|
1264
|
+
);
|
|
1265
|
+
if (renderResult.pageCount === 0) {
|
|
1266
|
+
this.logger.info(
|
|
1267
|
+
`[PdfLanguageDetector] No pages found; using default OCR languages: ${JSON.stringify(DEFAULT_OCR_LANGUAGES)}`
|
|
1268
|
+
);
|
|
1269
|
+
return {
|
|
1270
|
+
detectedLanguages: [...DEFAULT_OCR_LANGUAGES],
|
|
1271
|
+
reason: "No pages found in PDF",
|
|
1272
|
+
sampledPages: 0,
|
|
1273
|
+
totalPages: 0,
|
|
1274
|
+
source: "default"
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
const sampleIndices = this.selectSamplePages(
|
|
1278
|
+
renderResult.pageCount,
|
|
1279
|
+
options.maxSamplePages ?? DEFAULT_MAX_SAMPLE_PAGES
|
|
1280
|
+
);
|
|
1281
|
+
this.logger.info(
|
|
1282
|
+
`[PdfLanguageDetector] Sampling ${sampleIndices.length} of ${renderResult.pageCount} pages for language detection: [${sampleIndices.map((i) => i + 1).join(", ")}]`
|
|
1283
|
+
);
|
|
1284
|
+
const languageFrequency = /* @__PURE__ */ new Map();
|
|
1285
|
+
let sampledPages = 0;
|
|
1286
|
+
for (const index of sampleIndices) {
|
|
1287
|
+
const pageFile = renderResult.pageFiles[index];
|
|
1288
|
+
if (!pageFile) continue;
|
|
1289
|
+
sampledPages += 1;
|
|
1290
|
+
const detectedLanguages2 = await this.analyzeSamplePage(
|
|
1291
|
+
pageFile,
|
|
1292
|
+
index + 1,
|
|
1293
|
+
options
|
|
1294
|
+
);
|
|
1295
|
+
for (const language of detectedLanguages2) {
|
|
1296
|
+
languageFrequency.set(
|
|
1297
|
+
language,
|
|
1298
|
+
(languageFrequency.get(language) ?? 0) + 1
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const detectedLanguages = this.aggregateLanguages(languageFrequency);
|
|
1303
|
+
if (detectedLanguages.length === 0) {
|
|
1304
|
+
this.logger.info(
|
|
1305
|
+
`[PdfLanguageDetector] No languages detected from sampled pages; using default OCR languages: ${JSON.stringify(DEFAULT_OCR_LANGUAGES)}`
|
|
1306
|
+
);
|
|
1307
|
+
return {
|
|
1308
|
+
detectedLanguages: [...DEFAULT_OCR_LANGUAGES],
|
|
1309
|
+
reason: `No language detected in ${sampledPages} sampled pages`,
|
|
1310
|
+
sampledPages,
|
|
1311
|
+
totalPages: renderResult.pageCount,
|
|
1312
|
+
source: "default"
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
this.logger.info(
|
|
1316
|
+
`[PdfLanguageDetector] Languages detected from sampled pages: ${detectedLanguages.join(", ")}`
|
|
1317
|
+
);
|
|
1318
|
+
return {
|
|
1319
|
+
detectedLanguages,
|
|
1320
|
+
reason: `Languages detected in ${sampledPages} sampled pages`,
|
|
1321
|
+
sampledPages,
|
|
1322
|
+
totalPages: renderResult.pageCount,
|
|
1323
|
+
source: "vlm"
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
selectSamplePages(totalPages, maxSamples) {
|
|
1327
|
+
if (totalPages === 0) return [];
|
|
1328
|
+
if (totalPages <= maxSamples) {
|
|
1329
|
+
return Array.from({ length: totalPages }, (_, i) => i);
|
|
1330
|
+
}
|
|
1331
|
+
const trimCount = Math.max(1, Math.ceil(totalPages * EDGE_TRIM_RATIO));
|
|
1332
|
+
const start = trimCount;
|
|
1333
|
+
const end = totalPages - trimCount;
|
|
1334
|
+
const eligibleCount = end - start;
|
|
1335
|
+
if (eligibleCount <= 0) {
|
|
1336
|
+
return [Math.floor(totalPages / 2)];
|
|
1337
|
+
}
|
|
1338
|
+
if (eligibleCount <= maxSamples) {
|
|
1339
|
+
return Array.from({ length: eligibleCount }, (_, i) => start + i);
|
|
1340
|
+
}
|
|
1341
|
+
const step = eligibleCount / maxSamples;
|
|
1342
|
+
return Array.from(
|
|
1343
|
+
{ length: maxSamples },
|
|
1344
|
+
(_, i) => start + Math.floor(i * step)
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
async analyzeSamplePage(pageFile, pageNo, options) {
|
|
1348
|
+
this.logger.debug(
|
|
1349
|
+
`[PdfLanguageDetector] Analyzing page ${pageNo} for language...`
|
|
1350
|
+
);
|
|
1351
|
+
const imageData = new Uint8Array((0, import_node_fs.readFileSync)(pageFile));
|
|
1352
|
+
const result = await LLMCaller.callVision({
|
|
1353
|
+
schema: languageDetectionSchema,
|
|
1354
|
+
messages: [
|
|
1355
|
+
{
|
|
1356
|
+
role: "user",
|
|
1357
|
+
content: [
|
|
1358
|
+
{
|
|
1359
|
+
type: "text",
|
|
1360
|
+
text: KOREAN_DOCUMENT_DETECTION_PROMPT
|
|
1361
|
+
},
|
|
1362
|
+
{
|
|
1363
|
+
type: "image",
|
|
1364
|
+
image: imageData,
|
|
1365
|
+
mediaType: "image/png"
|
|
1366
|
+
}
|
|
1367
|
+
]
|
|
1368
|
+
}
|
|
1369
|
+
],
|
|
1370
|
+
primaryModel: options.model,
|
|
1371
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
1372
|
+
temperature: options.temperature ?? 0,
|
|
1373
|
+
abortSignal: options.abortSignal,
|
|
1374
|
+
component: "PdfLanguageDetector",
|
|
1375
|
+
phase: "language-detection"
|
|
1376
|
+
});
|
|
1377
|
+
options.aggregator?.track(result.usage);
|
|
1378
|
+
const output = result.output;
|
|
1379
|
+
const detectedLanguages = output.detectedLanguages.map(import_model.normalizeToBcp47).filter((tag) => tag !== null);
|
|
1380
|
+
this.logger.debug(
|
|
1381
|
+
`[PdfLanguageDetector] Page ${pageNo}: detectedLanguages=${detectedLanguages.join(",")}`
|
|
1382
|
+
);
|
|
1383
|
+
return detectedLanguages;
|
|
1384
|
+
}
|
|
1385
|
+
aggregateLanguages(languageFrequency) {
|
|
1386
|
+
return [...languageFrequency.entries()].sort((a, b) => b[1] - a[1]).map(([language]) => language);
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
// src/errors/image-pdf-fallback-error.ts
|
|
1391
|
+
var ImagePdfFallbackError = class extends Error {
|
|
1392
|
+
constructor(originalError, fallbackError) {
|
|
1393
|
+
super(
|
|
1394
|
+
`PDF conversion failed with fallback. Original: ${originalError.message}. Fallback: ${fallbackError.message}`
|
|
1395
|
+
);
|
|
1396
|
+
this.originalError = originalError;
|
|
1397
|
+
this.fallbackError = fallbackError;
|
|
1398
|
+
}
|
|
1399
|
+
name = "ImagePdfFallbackError";
|
|
1400
|
+
};
|
|
1401
|
+
|
|
1402
|
+
// src/processors/page-renderer.ts
|
|
1403
|
+
var import_node_fs2 = require("fs");
|
|
1404
|
+
var import_node_path3 = require("path");
|
|
1405
|
+
var PROGRESS_LOG_PERCENT_STEP = 10;
|
|
1406
|
+
var PageRenderer = class {
|
|
1407
|
+
constructor(logger) {
|
|
1408
|
+
this.logger = logger;
|
|
1409
|
+
}
|
|
1410
|
+
lastLoggedPercent = 0;
|
|
1411
|
+
/**
|
|
1412
|
+
* Render all pages of a PDF to individual PNG files.
|
|
1413
|
+
*
|
|
1414
|
+
* Uses per-page rendering (`magick 'input.pdf[N]'`) when page count is known,
|
|
1415
|
+
* limiting peak memory to ~15MB/page instead of loading all pages at once.
|
|
1416
|
+
*
|
|
1417
|
+
* @param pdfPath - Absolute path to the source PDF file
|
|
1418
|
+
* @param outputDir - Directory where pages/ subdirectory will be created
|
|
1419
|
+
* @param options - Rendering options
|
|
1420
|
+
* @returns Render result with page count and file paths
|
|
1421
|
+
*/
|
|
1422
|
+
async renderPages(pdfPath, outputDir, options) {
|
|
1423
|
+
const dpi = options?.dpi ?? PAGE_RENDERING.DEFAULT_DPI;
|
|
1424
|
+
const pagesDir = (0, import_node_path3.join)(outputDir, "pages");
|
|
1425
|
+
if (!(0, import_node_fs2.existsSync)(pagesDir)) {
|
|
1426
|
+
(0, import_node_fs2.mkdirSync)(pagesDir, { recursive: true });
|
|
1427
|
+
}
|
|
1428
|
+
const totalPages = await this.getPageCount(pdfPath);
|
|
1429
|
+
if (totalPages > 0) {
|
|
1430
|
+
this.logger.info(
|
|
1431
|
+
`[PageRenderer] Rendering ${totalPages} pages at ${dpi} DPI...`
|
|
1432
|
+
);
|
|
1433
|
+
this.lastLoggedPercent = 0;
|
|
1434
|
+
for (let i = 0; i < totalPages; i++) {
|
|
1435
|
+
const result = await spawnAsync(
|
|
1436
|
+
"magick",
|
|
1437
|
+
[
|
|
1438
|
+
"-density",
|
|
1439
|
+
dpi.toString(),
|
|
1440
|
+
`${pdfPath}[${i}]`,
|
|
1441
|
+
"-background",
|
|
1442
|
+
"white",
|
|
1443
|
+
"-alpha",
|
|
1444
|
+
"remove",
|
|
1445
|
+
"-alpha",
|
|
1446
|
+
"off",
|
|
1447
|
+
(0, import_node_path3.join)(pagesDir, `page_${i}.png`)
|
|
1448
|
+
],
|
|
1449
|
+
{ captureStdout: false }
|
|
1450
|
+
);
|
|
1451
|
+
if (result.code !== 0) {
|
|
1452
|
+
throw new Error(
|
|
1453
|
+
`[PageRenderer] Failed to render page ${i + 1}/${totalPages}: ${result.stderr || "Unknown error"}`
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
this.logProgress(i + 1, totalPages);
|
|
1457
|
+
}
|
|
1458
|
+
} else {
|
|
1459
|
+
this.logger.info(`[PageRenderer] Rendering PDF at ${dpi} DPI...`);
|
|
1460
|
+
const result = await spawnAsync(
|
|
1461
|
+
"magick",
|
|
1462
|
+
[
|
|
1463
|
+
"-density",
|
|
1464
|
+
dpi.toString(),
|
|
1465
|
+
pdfPath,
|
|
1466
|
+
"-background",
|
|
1467
|
+
"white",
|
|
1468
|
+
"-alpha",
|
|
1469
|
+
"remove",
|
|
1470
|
+
"-alpha",
|
|
1471
|
+
"off",
|
|
1472
|
+
(0, import_node_path3.join)(pagesDir, "page_%d.png")
|
|
1473
|
+
],
|
|
1474
|
+
{ captureStdout: false }
|
|
1475
|
+
);
|
|
1476
|
+
if (result.code !== 0) {
|
|
1477
|
+
throw new Error(
|
|
1478
|
+
`[PageRenderer] Failed to render PDF pages: ${result.stderr || "Unknown error"}`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
const pageFiles = (0, import_node_fs2.readdirSync)(pagesDir).filter((f) => f.startsWith("page_") && f.endsWith(".png")).sort((a, b) => {
|
|
1483
|
+
const numA = parseInt(a.replace("page_", "").replace(".png", ""), 10);
|
|
1484
|
+
const numB = parseInt(b.replace("page_", "").replace(".png", ""), 10);
|
|
1485
|
+
return numA - numB;
|
|
1486
|
+
}).map((f) => (0, import_node_path3.join)(pagesDir, f));
|
|
1487
|
+
this.logger.info(
|
|
1488
|
+
`[PageRenderer] Rendered ${pageFiles.length} pages to ${pagesDir}`
|
|
1489
|
+
);
|
|
1490
|
+
return {
|
|
1491
|
+
pageCount: pageFiles.length,
|
|
1492
|
+
pagesDir,
|
|
1493
|
+
pageFiles
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1496
|
+
/**
|
|
1497
|
+
* Log rendering progress at appropriate intervals (every 10%).
|
|
1498
|
+
*/
|
|
1499
|
+
logProgress(current, total) {
|
|
1500
|
+
const percent = Math.floor(current / total * 100);
|
|
1501
|
+
if (percent >= this.lastLoggedPercent + PROGRESS_LOG_PERCENT_STEP || current === total) {
|
|
1502
|
+
this.lastLoggedPercent = percent;
|
|
1503
|
+
this.logger.info(
|
|
1504
|
+
`[PageRenderer] Rendering pages: ${current}/${total} (${percent}%)`
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Get total page count using pdfinfo.
|
|
1510
|
+
* Returns 0 on failure (progress logging will be skipped).
|
|
1511
|
+
*/
|
|
1512
|
+
async getPageCount(pdfPath) {
|
|
1513
|
+
try {
|
|
1514
|
+
const result = await spawnAsync("pdfinfo", [pdfPath]);
|
|
1515
|
+
if (result.code !== 0) return 0;
|
|
1516
|
+
const match = result.stdout.match(/^Pages:\s+(\d+)/m);
|
|
1517
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
1518
|
+
} catch {
|
|
1519
|
+
return 0;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1159
1524
|
// src/validators/document-type-validator.ts
|
|
1160
1525
|
var import_zod = require("zod");
|
|
1161
1526
|
|
|
@@ -1247,7 +1612,7 @@ ${combinedText}`,
|
|
|
1247
1612
|
};
|
|
1248
1613
|
|
|
1249
1614
|
// src/core/chunked-pdf-converter.ts
|
|
1250
|
-
var
|
|
1615
|
+
var import_node_fs7 = require("fs");
|
|
1251
1616
|
var import_node_path7 = require("path");
|
|
1252
1617
|
|
|
1253
1618
|
// src/processors/docling-document-merger.ts
|
|
@@ -1380,15 +1745,15 @@ var DoclingDocumentMerger = class {
|
|
|
1380
1745
|
};
|
|
1381
1746
|
|
|
1382
1747
|
// src/processors/image-extractor.ts
|
|
1383
|
-
var
|
|
1384
|
-
var
|
|
1748
|
+
var import_node_fs4 = require("fs");
|
|
1749
|
+
var import_node_path4 = require("path");
|
|
1385
1750
|
var import_node_stream = require("stream");
|
|
1386
1751
|
var import_promises3 = require("stream/promises");
|
|
1387
1752
|
var yauzl = __toESM(require("yauzl"), 1);
|
|
1388
1753
|
|
|
1389
1754
|
// src/utils/jq.ts
|
|
1390
1755
|
var import_node_child_process3 = require("child_process");
|
|
1391
|
-
var
|
|
1756
|
+
var import_node_fs3 = require("fs");
|
|
1392
1757
|
var import_promises = require("fs/promises");
|
|
1393
1758
|
var import_promises2 = require("stream/promises");
|
|
1394
1759
|
function getJqPath() {
|
|
@@ -1459,7 +1824,7 @@ function runJqFileToFile(program, inputPath, outputPath) {
|
|
|
1459
1824
|
child.stderr.on("data", (chunk) => {
|
|
1460
1825
|
state.stderr += chunk;
|
|
1461
1826
|
});
|
|
1462
|
-
const ws = (0,
|
|
1827
|
+
const ws = (0, import_node_fs3.createWriteStream)(outputPath);
|
|
1463
1828
|
function trySettle() {
|
|
1464
1829
|
if (state.settled) return;
|
|
1465
1830
|
if (!state.pipelineDone || state.exitCode === null) return;
|
|
@@ -1593,9 +1958,9 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1593
1958
|
}
|
|
1594
1959
|
zipfile.readEntry();
|
|
1595
1960
|
zipfile.on("entry", (entry) => {
|
|
1596
|
-
const entryPath = (0,
|
|
1961
|
+
const entryPath = (0, import_node_path4.join)(targetDir, entry.fileName);
|
|
1597
1962
|
if (/\/$/.test(entry.fileName)) {
|
|
1598
|
-
(0,
|
|
1963
|
+
(0, import_node_fs4.mkdirSync)(entryPath, { recursive: true });
|
|
1599
1964
|
zipfile.readEntry();
|
|
1600
1965
|
} else {
|
|
1601
1966
|
zipfile.openReadStream(entry, (err2, readStream) => {
|
|
@@ -1603,8 +1968,8 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1603
1968
|
reject(err2 || new Error("Failed to open read stream"));
|
|
1604
1969
|
return;
|
|
1605
1970
|
}
|
|
1606
|
-
(0,
|
|
1607
|
-
const writeStream = (0,
|
|
1971
|
+
(0, import_node_fs4.mkdirSync)((0, import_node_path4.join)(entryPath, ".."), { recursive: true });
|
|
1972
|
+
const writeStream = (0, import_node_fs4.createWriteStream)(entryPath);
|
|
1608
1973
|
readStream.pipe(writeStream);
|
|
1609
1974
|
writeStream.on("finish", () => {
|
|
1610
1975
|
zipfile.readEntry();
|
|
@@ -1627,9 +1992,9 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1627
1992
|
const PREFIX = "data:image/png;base64,";
|
|
1628
1993
|
const base64Content = base64Data.startsWith(PREFIX) ? base64Data.slice(PREFIX.length) : base64Data;
|
|
1629
1994
|
const filename = `${prefix}_${index}.png`;
|
|
1630
|
-
const filepath = (0,
|
|
1995
|
+
const filepath = (0, import_node_path4.join)(imagesDir, filename);
|
|
1631
1996
|
const buffer = Buffer.from(base64Content, "base64");
|
|
1632
|
-
(0,
|
|
1997
|
+
(0, import_node_fs4.writeFileSync)(filepath, buffer);
|
|
1633
1998
|
return `${dirName}/${filename}`;
|
|
1634
1999
|
}
|
|
1635
2000
|
/**
|
|
@@ -1661,9 +2026,9 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1661
2026
|
}
|
|
1662
2027
|
const base64Content = pending.slice(dataStart, quoteIdx);
|
|
1663
2028
|
const filename = `image_${imageIndex}.png`;
|
|
1664
|
-
const filepath = (0,
|
|
2029
|
+
const filepath = (0, import_node_path4.join)(imagesDir, filename);
|
|
1665
2030
|
const buf = Buffer.from(base64Content, "base64");
|
|
1666
|
-
(0,
|
|
2031
|
+
(0, import_node_fs4.writeFileSync)(filepath, buf);
|
|
1667
2032
|
const relativePath = `images/${filename}`;
|
|
1668
2033
|
imageIndex++;
|
|
1669
2034
|
pending = pending.slice(quoteIdx + 1);
|
|
@@ -1688,8 +2053,8 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1688
2053
|
callback();
|
|
1689
2054
|
}
|
|
1690
2055
|
});
|
|
1691
|
-
const rs = (0,
|
|
1692
|
-
const ws = (0,
|
|
2056
|
+
const rs = (0, import_node_fs4.createReadStream)(htmlInputPath, { encoding: "utf-8" });
|
|
2057
|
+
const ws = (0, import_node_fs4.createWriteStream)(htmlOutputPath, { encoding: "utf-8" });
|
|
1693
2058
|
await (0, import_promises3.pipeline)(rs, transform, ws);
|
|
1694
2059
|
return imageIndex;
|
|
1695
2060
|
}
|
|
@@ -1705,19 +2070,19 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1705
2070
|
*/
|
|
1706
2071
|
static async saveDocumentsWithExtractedImages(logger, outputDir, filename, jsonSourcePath, htmlSourcePath) {
|
|
1707
2072
|
try {
|
|
1708
|
-
if ((0,
|
|
1709
|
-
(0,
|
|
2073
|
+
if ((0, import_node_fs4.existsSync)(outputDir)) {
|
|
2074
|
+
(0, import_node_fs4.rmSync)(outputDir, { recursive: true, force: true });
|
|
1710
2075
|
}
|
|
1711
2076
|
} catch (e) {
|
|
1712
2077
|
logger.warn("[PDFConverter] Failed to clear output directory:", e);
|
|
1713
2078
|
}
|
|
1714
|
-
(0,
|
|
1715
|
-
const baseName = filename.replace((0,
|
|
1716
|
-
const jsonPath = (0,
|
|
2079
|
+
(0, import_node_fs4.mkdirSync)(outputDir, { recursive: true });
|
|
2080
|
+
const baseName = filename.replace((0, import_node_path4.extname)(filename), "");
|
|
2081
|
+
const jsonPath = (0, import_node_path4.join)(outputDir, `${baseName}.json`);
|
|
1717
2082
|
try {
|
|
1718
|
-
const imagesDir = (0,
|
|
1719
|
-
if (!(0,
|
|
1720
|
-
(0,
|
|
2083
|
+
const imagesDir = (0, import_node_path4.join)(outputDir, "images");
|
|
2084
|
+
if (!(0, import_node_fs4.existsSync)(imagesDir)) {
|
|
2085
|
+
(0, import_node_fs4.mkdirSync)(imagesDir, { recursive: true });
|
|
1721
2086
|
}
|
|
1722
2087
|
const imageCount = await jqExtractBase64PngStringsStreaming(
|
|
1723
2088
|
jsonSourcePath,
|
|
@@ -1751,11 +2116,11 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1751
2116
|
throw e;
|
|
1752
2117
|
}
|
|
1753
2118
|
logger.info("[PDFConverter] Saved JSON:", jsonPath);
|
|
1754
|
-
const htmlPath = (0,
|
|
2119
|
+
const htmlPath = (0, import_node_path4.join)(outputDir, `${baseName}.html`);
|
|
1755
2120
|
try {
|
|
1756
|
-
const imagesDir = (0,
|
|
1757
|
-
if (!(0,
|
|
1758
|
-
(0,
|
|
2121
|
+
const imagesDir = (0, import_node_path4.join)(outputDir, "images");
|
|
2122
|
+
if (!(0, import_node_fs4.existsSync)(imagesDir)) {
|
|
2123
|
+
(0, import_node_fs4.mkdirSync)(imagesDir, { recursive: true });
|
|
1759
2124
|
}
|
|
1760
2125
|
const htmlImageCount = await _ImageExtractor.extractImagesFromHtmlStream(
|
|
1761
2126
|
htmlSourcePath,
|
|
@@ -1770,8 +2135,8 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1770
2135
|
"[PDFConverter] Failed to extract images from HTML, copying original. Error:",
|
|
1771
2136
|
e
|
|
1772
2137
|
);
|
|
1773
|
-
const rs = (0,
|
|
1774
|
-
const ws = (0,
|
|
2138
|
+
const rs = (0, import_node_fs4.createReadStream)(htmlSourcePath);
|
|
2139
|
+
const ws = (0, import_node_fs4.createWriteStream)(htmlPath);
|
|
1775
2140
|
await (0, import_promises3.pipeline)(rs, ws);
|
|
1776
2141
|
}
|
|
1777
2142
|
logger.info("[PDFConverter] Saved HTML:", htmlPath);
|
|
@@ -1792,16 +2157,16 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1792
2157
|
static async extractAndSaveDocumentsFromZip(logger, zipPath, extractDir, outputDir) {
|
|
1793
2158
|
logger.info("[PDFConverter] Extracting ZIP file...");
|
|
1794
2159
|
await _ImageExtractor.extractZip(zipPath, extractDir);
|
|
1795
|
-
const files = (0,
|
|
1796
|
-
const jsonFile = files.find((f) => (0,
|
|
1797
|
-
const htmlFile = files.find((f) => (0,
|
|
2160
|
+
const files = (0, import_node_fs4.readdirSync)(extractDir);
|
|
2161
|
+
const jsonFile = files.find((f) => (0, import_node_path4.extname)(f).toLowerCase() === ".json");
|
|
2162
|
+
const htmlFile = files.find((f) => (0, import_node_path4.extname)(f).toLowerCase() === ".html");
|
|
1798
2163
|
if (!jsonFile || !htmlFile) {
|
|
1799
2164
|
throw new Error(
|
|
1800
2165
|
`Expected one JSON and one HTML file in extracted directory. Found: ${files.join(", ")}`
|
|
1801
2166
|
);
|
|
1802
2167
|
}
|
|
1803
|
-
const jsonPath = (0,
|
|
1804
|
-
const htmlPath = (0,
|
|
2168
|
+
const jsonPath = (0, import_node_path4.join)(extractDir, jsonFile);
|
|
2169
|
+
const htmlPath = (0, import_node_path4.join)(extractDir, htmlFile);
|
|
1805
2170
|
logger.info("[PDFConverter] Saving converted files to output...");
|
|
1806
2171
|
await _ImageExtractor.saveDocumentsWithExtractedImages(
|
|
1807
2172
|
logger,
|
|
@@ -1815,7 +2180,7 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1815
2180
|
};
|
|
1816
2181
|
|
|
1817
2182
|
// src/utils/docling-result-downloader.ts
|
|
1818
|
-
var
|
|
2183
|
+
var import_node_fs5 = require("fs");
|
|
1819
2184
|
var import_promises4 = require("fs/promises");
|
|
1820
2185
|
var import_promises5 = require("stream/promises");
|
|
1821
2186
|
async function downloadTaskResult(client, taskId, zipPath, logger, logPrefix) {
|
|
@@ -1824,7 +2189,7 @@ ${logPrefix} Task completed, downloading ZIP file...`);
|
|
|
1824
2189
|
const zipResult = await client.getTaskResultFile(taskId);
|
|
1825
2190
|
logger.info(`${logPrefix} Saving ZIP file to:`, zipPath);
|
|
1826
2191
|
if (zipResult.fileStream) {
|
|
1827
|
-
const writeStream = (0,
|
|
2192
|
+
const writeStream = (0, import_node_fs5.createWriteStream)(zipPath);
|
|
1828
2193
|
await (0, import_promises5.pipeline)(zipResult.fileStream, writeStream);
|
|
1829
2194
|
return;
|
|
1830
2195
|
}
|
|
@@ -1849,9 +2214,9 @@ ${logPrefix} Task completed, downloading ZIP file...`);
|
|
|
1849
2214
|
}
|
|
1850
2215
|
|
|
1851
2216
|
// src/utils/local-file-server.ts
|
|
1852
|
-
var
|
|
2217
|
+
var import_node_fs6 = require("fs");
|
|
1853
2218
|
var import_node_http = require("http");
|
|
1854
|
-
var
|
|
2219
|
+
var import_node_path5 = require("path");
|
|
1855
2220
|
var LocalFileServer = class {
|
|
1856
2221
|
server = null;
|
|
1857
2222
|
port = 0;
|
|
@@ -1862,8 +2227,8 @@ var LocalFileServer = class {
|
|
|
1862
2227
|
* @returns URL to access the file
|
|
1863
2228
|
*/
|
|
1864
2229
|
async start(filePath) {
|
|
1865
|
-
const filename = (0,
|
|
1866
|
-
const stat = (0,
|
|
2230
|
+
const filename = (0, import_node_path5.basename)(filePath);
|
|
2231
|
+
const stat = (0, import_node_fs6.statSync)(filePath);
|
|
1867
2232
|
return new Promise((resolve, reject) => {
|
|
1868
2233
|
this.server = (0, import_node_http.createServer)((req, res) => {
|
|
1869
2234
|
if (req.url === `/${filename}`) {
|
|
@@ -1871,7 +2236,7 @@ var LocalFileServer = class {
|
|
|
1871
2236
|
"Content-Type": "application/pdf",
|
|
1872
2237
|
"Content-Length": stat.size
|
|
1873
2238
|
});
|
|
1874
|
-
(0,
|
|
2239
|
+
(0, import_node_fs6.createReadStream)(filePath).pipe(res);
|
|
1875
2240
|
} else {
|
|
1876
2241
|
res.writeHead(404);
|
|
1877
2242
|
res.end("Not Found");
|
|
@@ -1910,130 +2275,6 @@ var LocalFileServer = class {
|
|
|
1910
2275
|
// src/utils/page-image-updater.ts
|
|
1911
2276
|
var import_promises6 = require("fs/promises");
|
|
1912
2277
|
var import_node_path6 = require("path");
|
|
1913
|
-
|
|
1914
|
-
// src/processors/page-renderer.ts
|
|
1915
|
-
var import_node_fs5 = require("fs");
|
|
1916
|
-
var import_node_path5 = require("path");
|
|
1917
|
-
var PROGRESS_LOG_PERCENT_STEP = 10;
|
|
1918
|
-
var PageRenderer = class {
|
|
1919
|
-
constructor(logger) {
|
|
1920
|
-
this.logger = logger;
|
|
1921
|
-
}
|
|
1922
|
-
lastLoggedPercent = 0;
|
|
1923
|
-
/**
|
|
1924
|
-
* Render all pages of a PDF to individual PNG files.
|
|
1925
|
-
*
|
|
1926
|
-
* Uses per-page rendering (`magick 'input.pdf[N]'`) when page count is known,
|
|
1927
|
-
* limiting peak memory to ~15MB/page instead of loading all pages at once.
|
|
1928
|
-
*
|
|
1929
|
-
* @param pdfPath - Absolute path to the source PDF file
|
|
1930
|
-
* @param outputDir - Directory where pages/ subdirectory will be created
|
|
1931
|
-
* @param options - Rendering options
|
|
1932
|
-
* @returns Render result with page count and file paths
|
|
1933
|
-
*/
|
|
1934
|
-
async renderPages(pdfPath, outputDir, options) {
|
|
1935
|
-
const dpi = options?.dpi ?? PAGE_RENDERING.DEFAULT_DPI;
|
|
1936
|
-
const pagesDir = (0, import_node_path5.join)(outputDir, "pages");
|
|
1937
|
-
if (!(0, import_node_fs5.existsSync)(pagesDir)) {
|
|
1938
|
-
(0, import_node_fs5.mkdirSync)(pagesDir, { recursive: true });
|
|
1939
|
-
}
|
|
1940
|
-
const totalPages = await this.getPageCount(pdfPath);
|
|
1941
|
-
if (totalPages > 0) {
|
|
1942
|
-
this.logger.info(
|
|
1943
|
-
`[PageRenderer] Rendering ${totalPages} pages at ${dpi} DPI...`
|
|
1944
|
-
);
|
|
1945
|
-
this.lastLoggedPercent = 0;
|
|
1946
|
-
for (let i = 0; i < totalPages; i++) {
|
|
1947
|
-
const result = await spawnAsync(
|
|
1948
|
-
"magick",
|
|
1949
|
-
[
|
|
1950
|
-
"-density",
|
|
1951
|
-
dpi.toString(),
|
|
1952
|
-
`${pdfPath}[${i}]`,
|
|
1953
|
-
"-background",
|
|
1954
|
-
"white",
|
|
1955
|
-
"-alpha",
|
|
1956
|
-
"remove",
|
|
1957
|
-
"-alpha",
|
|
1958
|
-
"off",
|
|
1959
|
-
(0, import_node_path5.join)(pagesDir, `page_${i}.png`)
|
|
1960
|
-
],
|
|
1961
|
-
{ captureStdout: false }
|
|
1962
|
-
);
|
|
1963
|
-
if (result.code !== 0) {
|
|
1964
|
-
throw new Error(
|
|
1965
|
-
`[PageRenderer] Failed to render page ${i + 1}/${totalPages}: ${result.stderr || "Unknown error"}`
|
|
1966
|
-
);
|
|
1967
|
-
}
|
|
1968
|
-
this.logProgress(i + 1, totalPages);
|
|
1969
|
-
}
|
|
1970
|
-
} else {
|
|
1971
|
-
this.logger.info(`[PageRenderer] Rendering PDF at ${dpi} DPI...`);
|
|
1972
|
-
const result = await spawnAsync(
|
|
1973
|
-
"magick",
|
|
1974
|
-
[
|
|
1975
|
-
"-density",
|
|
1976
|
-
dpi.toString(),
|
|
1977
|
-
pdfPath,
|
|
1978
|
-
"-background",
|
|
1979
|
-
"white",
|
|
1980
|
-
"-alpha",
|
|
1981
|
-
"remove",
|
|
1982
|
-
"-alpha",
|
|
1983
|
-
"off",
|
|
1984
|
-
(0, import_node_path5.join)(pagesDir, "page_%d.png")
|
|
1985
|
-
],
|
|
1986
|
-
{ captureStdout: false }
|
|
1987
|
-
);
|
|
1988
|
-
if (result.code !== 0) {
|
|
1989
|
-
throw new Error(
|
|
1990
|
-
`[PageRenderer] Failed to render PDF pages: ${result.stderr || "Unknown error"}`
|
|
1991
|
-
);
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
const pageFiles = (0, import_node_fs5.readdirSync)(pagesDir).filter((f) => f.startsWith("page_") && f.endsWith(".png")).sort((a, b) => {
|
|
1995
|
-
const numA = parseInt(a.replace("page_", "").replace(".png", ""), 10);
|
|
1996
|
-
const numB = parseInt(b.replace("page_", "").replace(".png", ""), 10);
|
|
1997
|
-
return numA - numB;
|
|
1998
|
-
}).map((f) => (0, import_node_path5.join)(pagesDir, f));
|
|
1999
|
-
this.logger.info(
|
|
2000
|
-
`[PageRenderer] Rendered ${pageFiles.length} pages to ${pagesDir}`
|
|
2001
|
-
);
|
|
2002
|
-
return {
|
|
2003
|
-
pageCount: pageFiles.length,
|
|
2004
|
-
pagesDir,
|
|
2005
|
-
pageFiles
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
/**
|
|
2009
|
-
* Log rendering progress at appropriate intervals (every 10%).
|
|
2010
|
-
*/
|
|
2011
|
-
logProgress(current, total) {
|
|
2012
|
-
const percent = Math.floor(current / total * 100);
|
|
2013
|
-
if (percent >= this.lastLoggedPercent + PROGRESS_LOG_PERCENT_STEP || current === total) {
|
|
2014
|
-
this.lastLoggedPercent = percent;
|
|
2015
|
-
this.logger.info(
|
|
2016
|
-
`[PageRenderer] Rendering pages: ${current}/${total} (${percent}%)`
|
|
2017
|
-
);
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
/**
|
|
2021
|
-
* Get total page count using pdfinfo.
|
|
2022
|
-
* Returns 0 on failure (progress logging will be skipped).
|
|
2023
|
-
*/
|
|
2024
|
-
async getPageCount(pdfPath) {
|
|
2025
|
-
try {
|
|
2026
|
-
const result = await spawnAsync("pdfinfo", [pdfPath]);
|
|
2027
|
-
if (result.code !== 0) return 0;
|
|
2028
|
-
const match = result.stdout.match(/^Pages:\s+(\d+)/m);
|
|
2029
|
-
return match ? parseInt(match[1], 10) : 0;
|
|
2030
|
-
} catch {
|
|
2031
|
-
return 0;
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
};
|
|
2035
|
-
|
|
2036
|
-
// src/utils/page-image-updater.ts
|
|
2037
2278
|
async function renderAndUpdatePageImages(pdfPath, outputDir, logger, logPrefix) {
|
|
2038
2279
|
logger.info(`${logPrefix} Rendering page images with ImageMagick...`);
|
|
2039
2280
|
const renderer = new PageRenderer(logger);
|
|
@@ -2145,6 +2386,7 @@ ${logPrefix} Task failed after ${elapsed}s: ${errorDetails}`
|
|
|
2145
2386
|
// src/core/conversion-options-builder.ts
|
|
2146
2387
|
var import_es_toolkit = require("es-toolkit");
|
|
2147
2388
|
function buildConversionOptions(options) {
|
|
2389
|
+
const ocrLanguages = options.ocr_lang ?? DEFAULT_OCR_LANGUAGES;
|
|
2148
2390
|
return {
|
|
2149
2391
|
...(0, import_es_toolkit.omit)(options, [
|
|
2150
2392
|
"num_threads",
|
|
@@ -2157,14 +2399,18 @@ function buildConversionOptions(options) {
|
|
|
2157
2399
|
"chunkedConversion",
|
|
2158
2400
|
"chunkSize",
|
|
2159
2401
|
"chunkMaxRetries",
|
|
2160
|
-
"documentValidationModel"
|
|
2402
|
+
"documentValidationModel",
|
|
2403
|
+
"languageDetectionModel"
|
|
2161
2404
|
]),
|
|
2162
2405
|
to_formats: ["json", "html"],
|
|
2163
2406
|
image_export_mode: "embedded",
|
|
2407
|
+
// Docling currently reads this top-level field in addition to ocr_options.
|
|
2408
|
+
// Keep it populated even when language detection is skipped.
|
|
2409
|
+
ocr_lang: ocrLanguages,
|
|
2164
2410
|
ocr_engine: "ocrmac",
|
|
2165
2411
|
ocr_options: {
|
|
2166
2412
|
kind: "ocrmac",
|
|
2167
|
-
lang:
|
|
2413
|
+
lang: ocrLanguages,
|
|
2168
2414
|
recognition: "accurate",
|
|
2169
2415
|
framework: "livetext"
|
|
2170
2416
|
},
|
|
@@ -2237,7 +2483,7 @@ var ChunkedPDFConverter = class {
|
|
|
2237
2483
|
const [start, end] = chunks[i];
|
|
2238
2484
|
this.checkAbort(abortSignal);
|
|
2239
2485
|
const chunkDir = (0, import_node_path7.join)(chunksBaseDir, `_chunk_${i}`);
|
|
2240
|
-
(0,
|
|
2486
|
+
(0, import_node_fs7.mkdirSync)(chunkDir, { recursive: true });
|
|
2241
2487
|
const doc = await this.convertChunk(
|
|
2242
2488
|
i,
|
|
2243
2489
|
chunks.length,
|
|
@@ -2266,12 +2512,12 @@ var ChunkedPDFConverter = class {
|
|
|
2266
2512
|
this.logger.info(
|
|
2267
2513
|
`[ChunkedPDFConverter] Merged: ${merged.texts.length} texts, ${merged.pictures.length} pictures, ${merged.tables.length} tables, ${Object.keys(merged.pages).length} pages`
|
|
2268
2514
|
);
|
|
2269
|
-
(0,
|
|
2515
|
+
(0, import_node_fs7.mkdirSync)(outputDir, { recursive: true });
|
|
2270
2516
|
const imagesDir = (0, import_node_path7.join)(outputDir, "images");
|
|
2271
|
-
(0,
|
|
2517
|
+
(0, import_node_fs7.mkdirSync)(imagesDir, { recursive: true });
|
|
2272
2518
|
this.relocateImages(chunksBaseDir, chunks.length, imagesDir);
|
|
2273
2519
|
const resultPath = (0, import_node_path7.join)(outputDir, "result.json");
|
|
2274
|
-
(0,
|
|
2520
|
+
(0, import_node_fs7.writeFileSync)(resultPath, JSON.stringify(merged));
|
|
2275
2521
|
try {
|
|
2276
2522
|
await renderAndUpdatePageImages(
|
|
2277
2523
|
pdfPath,
|
|
@@ -2286,16 +2532,16 @@ var ChunkedPDFConverter = class {
|
|
|
2286
2532
|
);
|
|
2287
2533
|
await onComplete(outputDir);
|
|
2288
2534
|
} finally {
|
|
2289
|
-
if ((0,
|
|
2290
|
-
(0,
|
|
2535
|
+
if ((0, import_node_fs7.existsSync)(chunksBaseDir)) {
|
|
2536
|
+
(0, import_node_fs7.rmSync)(chunksBaseDir, { recursive: true, force: true });
|
|
2291
2537
|
}
|
|
2292
2538
|
if (cleanupAfterCallback) {
|
|
2293
2539
|
this.logger.info(
|
|
2294
2540
|
"[ChunkedPDFConverter] Cleaning up output directory:",
|
|
2295
2541
|
outputDir
|
|
2296
2542
|
);
|
|
2297
|
-
if ((0,
|
|
2298
|
-
(0,
|
|
2543
|
+
if ((0, import_node_fs7.existsSync)(outputDir)) {
|
|
2544
|
+
(0, import_node_fs7.rmSync)(outputDir, { recursive: true, force: true });
|
|
2299
2545
|
}
|
|
2300
2546
|
} else {
|
|
2301
2547
|
this.logger.info(
|
|
@@ -2370,9 +2616,9 @@ var ChunkedPDFConverter = class {
|
|
|
2370
2616
|
);
|
|
2371
2617
|
const resultJsonPath = (0, import_node_path7.join)(chunkOutputDir, "result.json");
|
|
2372
2618
|
const doc = await runJqFileJson(".", resultJsonPath);
|
|
2373
|
-
if ((0,
|
|
2374
|
-
if ((0,
|
|
2375
|
-
(0,
|
|
2619
|
+
if ((0, import_node_fs7.existsSync)(zipPath)) (0, import_node_fs7.rmSync)(zipPath, { force: true });
|
|
2620
|
+
if ((0, import_node_fs7.existsSync)(extractDir)) {
|
|
2621
|
+
(0, import_node_fs7.rmSync)(extractDir, { recursive: true, force: true });
|
|
2376
2622
|
}
|
|
2377
2623
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
2378
2624
|
if (attempt > 0) {
|
|
@@ -2440,8 +2686,8 @@ var ChunkedPDFConverter = class {
|
|
|
2440
2686
|
"output",
|
|
2441
2687
|
"images"
|
|
2442
2688
|
);
|
|
2443
|
-
if (!(0,
|
|
2444
|
-
const picFiles = (0,
|
|
2689
|
+
if (!(0, import_node_fs7.existsSync)(chunkImagesDir)) continue;
|
|
2690
|
+
const picFiles = (0, import_node_fs7.readdirSync)(chunkImagesDir).filter((f) => f.startsWith("pic_") && f.endsWith(".png")).sort((a, b) => {
|
|
2445
2691
|
const numA = parseInt(a.replace("pic_", "").replace(".png", ""), 10);
|
|
2446
2692
|
const numB = parseInt(b.replace("pic_", "").replace(".png", ""), 10);
|
|
2447
2693
|
return numA - numB;
|
|
@@ -2449,7 +2695,7 @@ var ChunkedPDFConverter = class {
|
|
|
2449
2695
|
for (let fileIdx = 0; fileIdx < picFiles.length; fileIdx++) {
|
|
2450
2696
|
const src = (0, import_node_path7.join)(chunkImagesDir, picFiles[fileIdx]);
|
|
2451
2697
|
const dest = (0, import_node_path7.join)(imagesDir, `pic_${picGlobalIndex + fileIdx}.png`);
|
|
2452
|
-
(0,
|
|
2698
|
+
(0, import_node_fs7.copyFileSync)(src, dest);
|
|
2453
2699
|
}
|
|
2454
2700
|
picGlobalIndex += picFiles.length;
|
|
2455
2701
|
}
|
|
@@ -2461,8 +2707,8 @@ var ChunkedPDFConverter = class {
|
|
|
2461
2707
|
"output",
|
|
2462
2708
|
"images"
|
|
2463
2709
|
);
|
|
2464
|
-
if (!(0,
|
|
2465
|
-
const imageFiles = (0,
|
|
2710
|
+
if (!(0, import_node_fs7.existsSync)(chunkImagesDir)) continue;
|
|
2711
|
+
const imageFiles = (0, import_node_fs7.readdirSync)(chunkImagesDir).filter((f) => f.startsWith("image_") && f.endsWith(".png")).sort((a, b) => {
|
|
2466
2712
|
const numA = parseInt(
|
|
2467
2713
|
a.replace("image_", "").replace(".png", ""),
|
|
2468
2714
|
10
|
|
@@ -2476,7 +2722,7 @@ var ChunkedPDFConverter = class {
|
|
|
2476
2722
|
for (let fileIdx = 0; fileIdx < imageFiles.length; fileIdx++) {
|
|
2477
2723
|
const src = (0, import_node_path7.join)(chunkImagesDir, imageFiles[fileIdx]);
|
|
2478
2724
|
const dest = (0, import_node_path7.join)(imagesDir, `image_${imageGlobalIndex + fileIdx}.png`);
|
|
2479
|
-
(0,
|
|
2725
|
+
(0, import_node_fs7.copyFileSync)(src, dest);
|
|
2480
2726
|
}
|
|
2481
2727
|
imageGlobalIndex += imageFiles.length;
|
|
2482
2728
|
}
|
|
@@ -2491,18 +2737,18 @@ var ChunkedPDFConverter = class {
|
|
|
2491
2737
|
* pages/page_N.png, these pic_ files become orphaned.
|
|
2492
2738
|
*/
|
|
2493
2739
|
cleanupOrphanedPicFiles(resultPath, imagesDir) {
|
|
2494
|
-
const content = (0,
|
|
2740
|
+
const content = (0, import_node_fs7.readFileSync)(resultPath, "utf-8");
|
|
2495
2741
|
const referencedPics = new Set(
|
|
2496
2742
|
[...content.matchAll(/images\/pic_\d+\.png/g)].map(
|
|
2497
2743
|
(m) => m[0].replace("images/", "")
|
|
2498
2744
|
)
|
|
2499
2745
|
);
|
|
2500
|
-
const picFiles = (0,
|
|
2746
|
+
const picFiles = (0, import_node_fs7.readdirSync)(imagesDir).filter(
|
|
2501
2747
|
(f) => f.startsWith("pic_") && f.endsWith(".png")
|
|
2502
2748
|
);
|
|
2503
2749
|
const orphanedFiles = picFiles.filter((file) => !referencedPics.has(file));
|
|
2504
2750
|
orphanedFiles.forEach((file) => {
|
|
2505
|
-
(0,
|
|
2751
|
+
(0, import_node_fs7.rmSync)((0, import_node_path7.join)(imagesDir, file), { force: true });
|
|
2506
2752
|
});
|
|
2507
2753
|
if (orphanedFiles.length > 0) {
|
|
2508
2754
|
this.logger.info(
|
|
@@ -2519,7 +2765,7 @@ var ChunkedPDFConverter = class {
|
|
|
2519
2765
|
let cumulative = 0;
|
|
2520
2766
|
for (let i = 0; i < totalChunks; i++) {
|
|
2521
2767
|
const dir = (0, import_node_path7.join)(chunksBaseDir, `_chunk_${i}`, "output", "images");
|
|
2522
|
-
const count = (0,
|
|
2768
|
+
const count = (0, import_node_fs7.existsSync)(dir) ? (0, import_node_fs7.readdirSync)(dir).filter(
|
|
2523
2769
|
(f) => f.startsWith("pic_") && f.endsWith(".png")
|
|
2524
2770
|
).length : 0;
|
|
2525
2771
|
offsets.push(cumulative);
|
|
@@ -2538,7 +2784,7 @@ var ChunkedPDFConverter = class {
|
|
|
2538
2784
|
};
|
|
2539
2785
|
|
|
2540
2786
|
// src/core/docling-conversion-executor.ts
|
|
2541
|
-
var
|
|
2787
|
+
var import_node_fs8 = require("fs");
|
|
2542
2788
|
var import_node_path8 = require("path");
|
|
2543
2789
|
var DoclingConversionExecutor = class {
|
|
2544
2790
|
constructor(logger, client, timeout) {
|
|
@@ -2631,19 +2877,19 @@ var DoclingConversionExecutor = class {
|
|
|
2631
2877
|
this.logger.info("[PDFConverter] Total time:", duration, "ms");
|
|
2632
2878
|
} finally {
|
|
2633
2879
|
this.logger.info("[PDFConverter] Cleaning up temporary files...");
|
|
2634
|
-
if ((0,
|
|
2635
|
-
(0,
|
|
2880
|
+
if ((0, import_node_fs8.existsSync)(zipPath)) {
|
|
2881
|
+
(0, import_node_fs8.rmSync)(zipPath, { force: true });
|
|
2636
2882
|
}
|
|
2637
|
-
if ((0,
|
|
2638
|
-
(0,
|
|
2883
|
+
if ((0, import_node_fs8.existsSync)(extractDir)) {
|
|
2884
|
+
(0, import_node_fs8.rmSync)(extractDir, { recursive: true, force: true });
|
|
2639
2885
|
}
|
|
2640
2886
|
if (cleanupAfterCallback) {
|
|
2641
2887
|
this.logger.info(
|
|
2642
2888
|
"[PDFConverter] Cleaning up output directory:",
|
|
2643
2889
|
outputDir
|
|
2644
2890
|
);
|
|
2645
|
-
if ((0,
|
|
2646
|
-
(0,
|
|
2891
|
+
if ((0, import_node_fs8.existsSync)(outputDir)) {
|
|
2892
|
+
(0, import_node_fs8.rmSync)(outputDir, { recursive: true, force: true });
|
|
2647
2893
|
}
|
|
2648
2894
|
} else {
|
|
2649
2895
|
this.logger.info("[PDFConverter] Output preserved at:", outputDir);
|
|
@@ -2701,7 +2947,7 @@ var DoclingConversionExecutor = class {
|
|
|
2701
2947
|
};
|
|
2702
2948
|
|
|
2703
2949
|
// src/core/image-pdf-converter.ts
|
|
2704
|
-
var
|
|
2950
|
+
var import_node_fs9 = require("fs");
|
|
2705
2951
|
var import_node_os2 = require("os");
|
|
2706
2952
|
var import_node_path9 = require("path");
|
|
2707
2953
|
var ImagePdfConverter = class {
|
|
@@ -2729,8 +2975,8 @@ var ImagePdfConverter = class {
|
|
|
2729
2975
|
this.logger.info("[ImagePdfConverter] Image PDF created:", outputPath);
|
|
2730
2976
|
return outputPath;
|
|
2731
2977
|
} finally {
|
|
2732
|
-
if ((0,
|
|
2733
|
-
(0,
|
|
2978
|
+
if ((0, import_node_fs9.existsSync)(inputPath)) {
|
|
2979
|
+
(0, import_node_fs9.rmSync)(inputPath, { force: true });
|
|
2734
2980
|
}
|
|
2735
2981
|
}
|
|
2736
2982
|
}
|
|
@@ -2777,23 +3023,23 @@ var ImagePdfConverter = class {
|
|
|
2777
3023
|
* Cleanup the temporary image PDF file
|
|
2778
3024
|
*/
|
|
2779
3025
|
cleanup(imagePdfPath) {
|
|
2780
|
-
if ((0,
|
|
3026
|
+
if ((0, import_node_fs9.existsSync)(imagePdfPath)) {
|
|
2781
3027
|
this.logger.info(
|
|
2782
3028
|
"[ImagePdfConverter] Cleaning up temp file:",
|
|
2783
3029
|
imagePdfPath
|
|
2784
3030
|
);
|
|
2785
|
-
(0,
|
|
3031
|
+
(0, import_node_fs9.rmSync)(imagePdfPath, { force: true });
|
|
2786
3032
|
}
|
|
2787
3033
|
}
|
|
2788
3034
|
};
|
|
2789
3035
|
|
|
2790
3036
|
// src/core/post-docling-correction-pipeline.ts
|
|
2791
|
-
var
|
|
3037
|
+
var import_node_fs16 = require("fs");
|
|
2792
3038
|
var import_node_path16 = require("path");
|
|
2793
3039
|
|
|
2794
3040
|
// src/processors/post-docling-page-processor.ts
|
|
2795
|
-
var
|
|
2796
|
-
var
|
|
3041
|
+
var import_model2 = require("@heripo/model");
|
|
3042
|
+
var import_node_fs11 = require("fs");
|
|
2797
3043
|
var import_node_path11 = require("path");
|
|
2798
3044
|
|
|
2799
3045
|
// src/prompts/text-correction-prompt.ts
|
|
@@ -2850,35 +3096,35 @@ Substitution rules for tc:
|
|
|
2850
3096
|
If all correct: {"tc":[],"cc":[]}`;
|
|
2851
3097
|
|
|
2852
3098
|
// src/types/vlm-text-correction-schema.ts
|
|
2853
|
-
var
|
|
2854
|
-
var vlmTextCorrectionSchema =
|
|
3099
|
+
var import_v42 = require("zod/v4");
|
|
3100
|
+
var vlmTextCorrectionSchema = import_v42.z.object({
|
|
2855
3101
|
/** Text element corrections (substitution-based) */
|
|
2856
|
-
tc:
|
|
2857
|
-
|
|
3102
|
+
tc: import_v42.z.array(
|
|
3103
|
+
import_v42.z.object({
|
|
2858
3104
|
/** Text element index (from prompt) */
|
|
2859
|
-
i:
|
|
3105
|
+
i: import_v42.z.number().int().nonnegative(),
|
|
2860
3106
|
/** Substitutions: find/replace pairs applied left-to-right */
|
|
2861
|
-
s:
|
|
2862
|
-
|
|
3107
|
+
s: import_v42.z.array(
|
|
3108
|
+
import_v42.z.object({
|
|
2863
3109
|
/** Exact garbled substring to find */
|
|
2864
|
-
f:
|
|
3110
|
+
f: import_v42.z.string(),
|
|
2865
3111
|
/** Corrected replacement text */
|
|
2866
|
-
r:
|
|
3112
|
+
r: import_v42.z.string()
|
|
2867
3113
|
})
|
|
2868
3114
|
)
|
|
2869
3115
|
})
|
|
2870
3116
|
),
|
|
2871
3117
|
/** Table cell corrections */
|
|
2872
|
-
cc:
|
|
2873
|
-
|
|
3118
|
+
cc: import_v42.z.array(
|
|
3119
|
+
import_v42.z.object({
|
|
2874
3120
|
/** Table index (within the page) */
|
|
2875
|
-
ti:
|
|
3121
|
+
ti: import_v42.z.number().int().nonnegative(),
|
|
2876
3122
|
/** Row index */
|
|
2877
|
-
r:
|
|
3123
|
+
r: import_v42.z.number().int().nonnegative(),
|
|
2878
3124
|
/** Column index */
|
|
2879
|
-
c:
|
|
3125
|
+
c: import_v42.z.number().int().nonnegative(),
|
|
2880
3126
|
/** Corrected cell text */
|
|
2881
|
-
t:
|
|
3127
|
+
t: import_v42.z.string()
|
|
2882
3128
|
})
|
|
2883
3129
|
)
|
|
2884
3130
|
});
|
|
@@ -3016,21 +3262,21 @@ function applyCorrections(doc, pageNo, corrections, logger) {
|
|
|
3016
3262
|
}
|
|
3017
3263
|
|
|
3018
3264
|
// src/processors/review-assistance/review-assistance-page-gate.ts
|
|
3019
|
-
var
|
|
3265
|
+
var import_node_fs10 = require("fs");
|
|
3020
3266
|
var import_node_path10 = require("path");
|
|
3021
|
-
var
|
|
3267
|
+
var import_v43 = require("zod/v4");
|
|
3022
3268
|
var REVIEW_ASSISTANCE_PAGE_GATE_FILE = "review_assistance_page_gate.json";
|
|
3023
3269
|
var REVIEW_ASSISTANCE_PAGE_GATE_MAX_RETRIES = 2;
|
|
3024
3270
|
var REVIEW_ASSISTANCE_PAGE_GATE_TEMPERATURE = 0;
|
|
3025
3271
|
var MAX_REASON_COUNT = 12;
|
|
3026
3272
|
var MAX_CONTEXT_TEXT_LENGTH = 1800;
|
|
3027
3273
|
var REVIEW_ASSISTANCE_PAGE_IMAGE_NOT_AVAILABLE_REASON = "page_image_not_available";
|
|
3028
|
-
var reviewAssistancePageGateSchema =
|
|
3029
|
-
eligible:
|
|
3030
|
-
kind:
|
|
3031
|
-
score:
|
|
3032
|
-
reasons:
|
|
3033
|
-
exclusionReasons:
|
|
3274
|
+
var reviewAssistancePageGateSchema = import_v43.z.object({
|
|
3275
|
+
eligible: import_v43.z.boolean(),
|
|
3276
|
+
kind: import_v43.z.enum(["toc", "archaeological_data", "non_meaningful"]),
|
|
3277
|
+
score: import_v43.z.number().min(0).max(100),
|
|
3278
|
+
reasons: import_v43.z.array(import_v43.z.string()).max(MAX_REASON_COUNT),
|
|
3279
|
+
exclusionReasons: import_v43.z.array(import_v43.z.string()).max(MAX_REASON_COUNT)
|
|
3034
3280
|
});
|
|
3035
3281
|
var REVIEW_ASSISTANCE_PAGE_GATE_PROMPT = `You are deciding whether a single PDF page should run structural Review Assistance after OCR text correction.
|
|
3036
3282
|
|
|
@@ -3165,9 +3411,9 @@ function isReviewAssistancePageGatePending(eligibility) {
|
|
|
3165
3411
|
}
|
|
3166
3412
|
function readReviewAssistancePageGateReport(outputDir) {
|
|
3167
3413
|
const path = (0, import_node_path10.join)(outputDir, REVIEW_ASSISTANCE_PAGE_GATE_FILE);
|
|
3168
|
-
if (!(0,
|
|
3414
|
+
if (!(0, import_node_fs10.existsSync)(path)) return void 0;
|
|
3169
3415
|
const report = JSON.parse(
|
|
3170
|
-
(0,
|
|
3416
|
+
(0, import_node_fs10.readFileSync)(path, "utf-8")
|
|
3171
3417
|
);
|
|
3172
3418
|
return new Map(report.pages.map((page) => [page.pageNo, page]));
|
|
3173
3419
|
}
|
|
@@ -3177,7 +3423,7 @@ function writeReviewAssistancePageGateReport(outputDir, pages) {
|
|
|
3177
3423
|
version: "1.0",
|
|
3178
3424
|
pages: [...pages].sort((a, b) => a.pageNo - b.pageNo)
|
|
3179
3425
|
};
|
|
3180
|
-
(0,
|
|
3426
|
+
(0, import_node_fs10.writeFileSync)(
|
|
3181
3427
|
(0, import_node_path10.join)(outputDir, REVIEW_ASSISTANCE_PAGE_GATE_FILE),
|
|
3182
3428
|
JSON.stringify(report, null, 2)
|
|
3183
3429
|
);
|
|
@@ -3185,7 +3431,7 @@ function writeReviewAssistancePageGateReport(outputDir, pages) {
|
|
|
3185
3431
|
|
|
3186
3432
|
// src/processors/post-docling-page-processor.ts
|
|
3187
3433
|
var DEFAULT_CONCURRENCY = 1;
|
|
3188
|
-
var
|
|
3434
|
+
var DEFAULT_MAX_RETRIES2 = 3;
|
|
3189
3435
|
var DEFAULT_TEMPERATURE = 0;
|
|
3190
3436
|
var PostDoclingPageProcessor = class {
|
|
3191
3437
|
constructor(logger) {
|
|
@@ -3203,7 +3449,7 @@ var PostDoclingPageProcessor = class {
|
|
|
3203
3449
|
async correctAndSave(outputDir, model, options) {
|
|
3204
3450
|
this.logger.info("[PostDoclingPageProcessor] Starting text correction...");
|
|
3205
3451
|
const resultPath = (0, import_node_path11.join)(outputDir, "result.json");
|
|
3206
|
-
const doc = JSON.parse((0,
|
|
3452
|
+
const doc = JSON.parse((0, import_node_fs11.readFileSync)(resultPath, "utf-8"));
|
|
3207
3453
|
const allPageNumbers = this.getPageNumbers(doc);
|
|
3208
3454
|
if (allPageNumbers.length === 0) {
|
|
3209
3455
|
this.logger.info("[PostDoclingPageProcessor] No pages to process");
|
|
@@ -3247,7 +3493,7 @@ var PostDoclingPageProcessor = class {
|
|
|
3247
3493
|
if (corrections === null) return;
|
|
3248
3494
|
applyCorrections(doc, pageNo, corrections, this.logger);
|
|
3249
3495
|
});
|
|
3250
|
-
(0,
|
|
3496
|
+
(0, import_node_fs11.writeFileSync)(resultPath, JSON.stringify(doc, null, 2));
|
|
3251
3497
|
const gateResults = results.map((result) => result.reviewAssistanceEligibility).filter(
|
|
3252
3498
|
(result) => result !== void 0
|
|
3253
3499
|
);
|
|
@@ -3345,7 +3591,7 @@ var PostDoclingPageProcessor = class {
|
|
|
3345
3591
|
}
|
|
3346
3592
|
],
|
|
3347
3593
|
primaryModel: model,
|
|
3348
|
-
maxRetries: options?.maxRetries ??
|
|
3594
|
+
maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES2,
|
|
3349
3595
|
temperature: options?.temperature ?? DEFAULT_TEMPERATURE,
|
|
3350
3596
|
abortSignal: options?.abortSignal,
|
|
3351
3597
|
component: "PostDoclingPageProcessor",
|
|
@@ -3482,7 +3728,7 @@ var PostDoclingPageProcessor = class {
|
|
|
3482
3728
|
if (!documentLanguages?.length) {
|
|
3483
3729
|
return TEXT_CORRECTION_SYSTEM_PROMPT;
|
|
3484
3730
|
}
|
|
3485
|
-
const languageDesc = (0,
|
|
3731
|
+
const languageDesc = (0, import_model2.buildLanguageDescription)(documentLanguages);
|
|
3486
3732
|
const prefix = `LANGUAGE CONTEXT: This document is ${languageDesc}. Focus on correcting characters that do not match this language.
|
|
3487
3733
|
|
|
3488
3734
|
`;
|
|
@@ -3511,21 +3757,21 @@ var PostDoclingPageProcessor = class {
|
|
|
3511
3757
|
*/
|
|
3512
3758
|
readPageImage(outputDir, pageNo) {
|
|
3513
3759
|
const imagePath = (0, import_node_path11.join)(outputDir, "pages", `page_${pageNo - 1}.png`);
|
|
3514
|
-
return new Uint8Array((0,
|
|
3760
|
+
return new Uint8Array((0, import_node_fs11.readFileSync)(imagePath));
|
|
3515
3761
|
}
|
|
3516
3762
|
};
|
|
3517
3763
|
|
|
3518
3764
|
// src/processors/review-assistance/review-assistance-runner.ts
|
|
3519
|
-
var
|
|
3765
|
+
var import_node_fs15 = require("fs");
|
|
3520
3766
|
var import_promises7 = require("fs/promises");
|
|
3521
3767
|
var import_node_path15 = require("path");
|
|
3522
3768
|
|
|
3523
3769
|
// src/types/review-assistance-schema.ts
|
|
3524
|
-
var
|
|
3770
|
+
var import_v44 = require("zod/v4");
|
|
3525
3771
|
var REVIEW_ASSISTANCE_RATIONALE_MAX_LENGTH = 2e3;
|
|
3526
3772
|
var REVIEW_ASSISTANCE_EVIDENCE_MAX_LENGTH = 2e3;
|
|
3527
3773
|
var REVIEW_ASSISTANCE_PAGE_NOTE_MAX_LENGTH = 2e3;
|
|
3528
|
-
var reviewAssistanceCommandOpSchema =
|
|
3774
|
+
var reviewAssistanceCommandOpSchema = import_v44.z.enum([
|
|
3529
3775
|
"replaceText",
|
|
3530
3776
|
"addText",
|
|
3531
3777
|
"updateTextRole",
|
|
@@ -3543,18 +3789,18 @@ var reviewAssistanceCommandOpSchema = import_v43.z.enum([
|
|
|
3543
3789
|
"linkFootnote",
|
|
3544
3790
|
"moveNode"
|
|
3545
3791
|
]);
|
|
3546
|
-
var reviewAssistanceRawCommandSchema =
|
|
3792
|
+
var reviewAssistanceRawCommandSchema = import_v44.z.object({
|
|
3547
3793
|
op: reviewAssistanceCommandOpSchema,
|
|
3548
|
-
targetRef:
|
|
3549
|
-
payload:
|
|
3550
|
-
confidence:
|
|
3551
|
-
rationale:
|
|
3552
|
-
evidence:
|
|
3794
|
+
targetRef: import_v44.z.string().nullable(),
|
|
3795
|
+
payload: import_v44.z.record(import_v44.z.string(), import_v44.z.unknown()),
|
|
3796
|
+
confidence: import_v44.z.number().min(0).max(1),
|
|
3797
|
+
rationale: import_v44.z.string().max(REVIEW_ASSISTANCE_RATIONALE_MAX_LENGTH),
|
|
3798
|
+
evidence: import_v44.z.string().max(REVIEW_ASSISTANCE_EVIDENCE_MAX_LENGTH).nullable()
|
|
3553
3799
|
});
|
|
3554
|
-
var reviewAssistancePageSchema =
|
|
3555
|
-
pageNo:
|
|
3556
|
-
commands:
|
|
3557
|
-
pageNotes:
|
|
3800
|
+
var reviewAssistancePageSchema = import_v44.z.object({
|
|
3801
|
+
pageNo: import_v44.z.number().int().positive(),
|
|
3802
|
+
commands: import_v44.z.array(reviewAssistanceRawCommandSchema).max(80),
|
|
3803
|
+
pageNotes: import_v44.z.array(import_v44.z.string().max(REVIEW_ASSISTANCE_PAGE_NOTE_MAX_LENGTH)).max(10)
|
|
3558
3804
|
});
|
|
3559
3805
|
|
|
3560
3806
|
// src/prompts/review-assistance-prompt.ts
|
|
@@ -5047,7 +5293,7 @@ var PictureSplitCandidateDetector = class {
|
|
|
5047
5293
|
};
|
|
5048
5294
|
|
|
5049
5295
|
// src/processors/review-assistance/review-assistance-checkpoint-store.ts
|
|
5050
|
-
var
|
|
5296
|
+
var import_node_fs12 = require("fs");
|
|
5051
5297
|
var import_node_path13 = require("path");
|
|
5052
5298
|
var CHECKPOINT_FILE = "review_assistance_checkpoint.json";
|
|
5053
5299
|
var ReviewAssistanceCheckpointStore = class _ReviewAssistanceCheckpointStore {
|
|
@@ -5121,15 +5367,15 @@ var ReviewAssistanceCheckpointStore = class _ReviewAssistanceCheckpointStore {
|
|
|
5121
5367
|
}
|
|
5122
5368
|
write() {
|
|
5123
5369
|
this.checkpoint.updatedAt = this.now().toISOString();
|
|
5124
|
-
(0,
|
|
5370
|
+
(0, import_node_fs12.writeFileSync)(this.path, JSON.stringify(this.checkpoint, null, 2));
|
|
5125
5371
|
}
|
|
5126
5372
|
static read(path, runId, now) {
|
|
5127
|
-
if (!(0,
|
|
5373
|
+
if (!(0, import_node_fs12.existsSync)(path)) {
|
|
5128
5374
|
return this.empty(runId, now);
|
|
5129
5375
|
}
|
|
5130
5376
|
try {
|
|
5131
5377
|
const parsed = JSON.parse(
|
|
5132
|
-
(0,
|
|
5378
|
+
(0, import_node_fs12.readFileSync)(path, "utf-8")
|
|
5133
5379
|
);
|
|
5134
5380
|
if (parsed.schemaVersion !== 1 || parsed.runId !== runId) {
|
|
5135
5381
|
return this.empty(runId, now);
|
|
@@ -5419,10 +5665,10 @@ var ReviewAssistanceContextPacker = class {
|
|
|
5419
5665
|
};
|
|
5420
5666
|
|
|
5421
5667
|
// src/processors/review-assistance/review-assistance-patcher.ts
|
|
5422
|
-
var
|
|
5668
|
+
var import_node_fs14 = require("fs");
|
|
5423
5669
|
|
|
5424
5670
|
// src/processors/review-assistance/image-crop-writer.ts
|
|
5425
|
-
var
|
|
5671
|
+
var import_node_fs13 = require("fs");
|
|
5426
5672
|
var import_node_path14 = require("path");
|
|
5427
5673
|
var ImageCropWriter = class {
|
|
5428
5674
|
constructor(logger, snapper) {
|
|
@@ -5432,13 +5678,13 @@ var ImageCropWriter = class {
|
|
|
5432
5678
|
snapper;
|
|
5433
5679
|
async writeCrop(input) {
|
|
5434
5680
|
const imagesDir = (0, import_node_path14.join)(input.outputDir, "images");
|
|
5435
|
-
if (!(0,
|
|
5436
|
-
(0,
|
|
5681
|
+
if (!(0, import_node_fs13.existsSync)(imagesDir)) {
|
|
5682
|
+
(0, import_node_fs13.mkdirSync)(imagesDir, { recursive: true });
|
|
5437
5683
|
}
|
|
5438
5684
|
const filename = this.buildFilename(input);
|
|
5439
5685
|
const imageUri = `images/${filename}`;
|
|
5440
5686
|
const outputPath = (0, import_node_path14.join)(input.outputDir, imageUri);
|
|
5441
|
-
if ((0,
|
|
5687
|
+
if ((0, import_node_fs13.existsSync)(outputPath)) {
|
|
5442
5688
|
return { imageUri, outputPath, created: false };
|
|
5443
5689
|
}
|
|
5444
5690
|
const dims = await this.snapper.getImageDimensions(input.pageImagePath);
|
|
@@ -5461,7 +5707,7 @@ var ImageCropWriter = class {
|
|
|
5461
5707
|
outputPath
|
|
5462
5708
|
]);
|
|
5463
5709
|
if (result.code !== 0) {
|
|
5464
|
-
(0,
|
|
5710
|
+
(0, import_node_fs13.rmSync)(outputPath, { force: true });
|
|
5465
5711
|
throw new Error(
|
|
5466
5712
|
`[ImageCropWriter] Failed to write crop: ${result.stderr || "Unknown error"}`
|
|
5467
5713
|
);
|
|
@@ -6331,7 +6577,7 @@ var ReviewAssistancePatcher = class {
|
|
|
6331
6577
|
removeFiles(paths) {
|
|
6332
6578
|
for (const path of paths) {
|
|
6333
6579
|
try {
|
|
6334
|
-
(0,
|
|
6580
|
+
(0, import_node_fs14.rmSync)(path, { force: true });
|
|
6335
6581
|
} catch (error) {
|
|
6336
6582
|
this.logger.warn(
|
|
6337
6583
|
`[ReviewAssistancePatcher] Failed to remove generated artifact ${path}`,
|
|
@@ -7911,14 +8157,14 @@ var ReviewAssistanceRunner = class {
|
|
|
7911
8157
|
const resultPath = (0, import_node_path15.join)(outputDir, "result.json");
|
|
7912
8158
|
const ocrOriginPath = (0, import_node_path15.join)(outputDir, "result_ocr_origin.json");
|
|
7913
8159
|
const reviewOriginPath = (0, import_node_path15.join)(outputDir, "result_review_origin.json");
|
|
7914
|
-
if (!(0,
|
|
7915
|
-
(0,
|
|
8160
|
+
if (!(0, import_node_fs15.existsSync)(reviewOriginPath)) {
|
|
8161
|
+
(0, import_node_fs15.copyFileSync)(resultPath, reviewOriginPath);
|
|
7916
8162
|
}
|
|
7917
|
-
if (!(0,
|
|
7918
|
-
(0,
|
|
8163
|
+
if (!(0, import_node_fs15.existsSync)(ocrOriginPath)) {
|
|
8164
|
+
(0, import_node_fs15.copyFileSync)(reviewOriginPath, ocrOriginPath);
|
|
7919
8165
|
}
|
|
7920
8166
|
const doc = JSON.parse(
|
|
7921
|
-
(0,
|
|
8167
|
+
(0, import_node_fs15.readFileSync)(reviewOriginPath, "utf-8")
|
|
7922
8168
|
);
|
|
7923
8169
|
const pageTexts = options.pageTexts ?? await PdfTextExtractor.tryExtract(
|
|
7924
8170
|
this.logger,
|
|
@@ -8017,7 +8263,7 @@ var ReviewAssistanceRunner = class {
|
|
|
8017
8263
|
outputDir,
|
|
8018
8264
|
contexts
|
|
8019
8265
|
});
|
|
8020
|
-
(0,
|
|
8266
|
+
(0, import_node_fs15.writeFileSync)(resultPath, JSON.stringify(patched.doc, null, 2));
|
|
8021
8267
|
this.emitProgress(options, {
|
|
8022
8268
|
substage: "review-assistance:patch",
|
|
8023
8269
|
status: "completed",
|
|
@@ -8048,7 +8294,7 @@ var ReviewAssistanceRunner = class {
|
|
|
8048
8294
|
reportId,
|
|
8049
8295
|
pageCount: contexts.length
|
|
8050
8296
|
});
|
|
8051
|
-
(0,
|
|
8297
|
+
(0, import_node_fs15.writeFileSync)(
|
|
8052
8298
|
(0, import_node_path15.join)(outputDir, "review_assistance.json"),
|
|
8053
8299
|
JSON.stringify(report, null, 2)
|
|
8054
8300
|
);
|
|
@@ -9335,7 +9581,7 @@ var PostDoclingCorrectionPipeline = class {
|
|
|
9335
9581
|
const pageTexts = await this.extractPageTexts(pdfPath, outputDir);
|
|
9336
9582
|
const resultPath = (0, import_node_path16.join)(outputDir, "result.json");
|
|
9337
9583
|
const ocrOriginPath = (0, import_node_path16.join)(outputDir, "result_ocr_origin.json");
|
|
9338
|
-
(0,
|
|
9584
|
+
(0, import_node_fs16.copyFileSync)(resultPath, ocrOriginPath);
|
|
9339
9585
|
const pageProcessor = new PostDoclingPageProcessor(this.logger);
|
|
9340
9586
|
await pageProcessor.correctAndSave(
|
|
9341
9587
|
outputDir,
|
|
@@ -9441,8 +9687,13 @@ var PDFConverter = class {
|
|
|
9441
9687
|
async convert(url, reportId, onComplete, cleanupAfterCallback, options, abortSignal) {
|
|
9442
9688
|
this.logger.info("[PDFConverter] Converting:", url);
|
|
9443
9689
|
const aggregator = options.aggregator ?? new LLMTokenUsageAggregator();
|
|
9444
|
-
const trackedOptions = { ...options, aggregator };
|
|
9445
9690
|
const pdfPath = url.startsWith("file://") ? url.slice(7) : void 0;
|
|
9691
|
+
const trackedOptions = await this.withDetectedLanguages(
|
|
9692
|
+
pdfPath,
|
|
9693
|
+
reportId,
|
|
9694
|
+
{ ...options, aggregator },
|
|
9695
|
+
abortSignal
|
|
9696
|
+
);
|
|
9446
9697
|
const correctedOnComplete = new PostDoclingCorrectionPipeline(
|
|
9447
9698
|
this.logger
|
|
9448
9699
|
).wrapCallback(pdfPath, reportId, trackedOptions, onComplete, abortSignal);
|
|
@@ -9505,6 +9756,58 @@ var PDFConverter = class {
|
|
|
9505
9756
|
}
|
|
9506
9757
|
return this.buildTokenReport(aggregator) ?? conversionReport;
|
|
9507
9758
|
}
|
|
9759
|
+
async withDetectedLanguages(pdfPath, reportId, options, abortSignal) {
|
|
9760
|
+
if (options.ocr_lang && options.ocr_lang.length > 0) {
|
|
9761
|
+
this.logger.info(
|
|
9762
|
+
`[PDFConverter] OCR languages provided: ${JSON.stringify(options.ocr_lang)}`
|
|
9763
|
+
);
|
|
9764
|
+
return options;
|
|
9765
|
+
}
|
|
9766
|
+
if (!pdfPath) {
|
|
9767
|
+
this.logger.info(
|
|
9768
|
+
"[PDFConverter] Local PDF not available; OCR languages will use defaults"
|
|
9769
|
+
);
|
|
9770
|
+
return {
|
|
9771
|
+
...options,
|
|
9772
|
+
ocr_lang: [...DEFAULT_OCR_LANGUAGES]
|
|
9773
|
+
};
|
|
9774
|
+
}
|
|
9775
|
+
const languageDetectionDir = (0, import_node_path17.join)(
|
|
9776
|
+
process.cwd(),
|
|
9777
|
+
"output",
|
|
9778
|
+
reportId,
|
|
9779
|
+
"_language_detection"
|
|
9780
|
+
);
|
|
9781
|
+
const detector = new PdfLanguageDetector(
|
|
9782
|
+
this.logger,
|
|
9783
|
+
new PageRenderer(this.logger),
|
|
9784
|
+
new PdfTextExtractor(this.logger)
|
|
9785
|
+
);
|
|
9786
|
+
try {
|
|
9787
|
+
const result = await detector.detect(pdfPath, languageDetectionDir, {
|
|
9788
|
+
model: options.languageDetectionModel,
|
|
9789
|
+
aggregator: options.aggregator,
|
|
9790
|
+
abortSignal
|
|
9791
|
+
});
|
|
9792
|
+
this.logger.info(
|
|
9793
|
+
`[PDFConverter] OCR languages detected: ${JSON.stringify(result.detectedLanguages)} (${result.reason})`
|
|
9794
|
+
);
|
|
9795
|
+
if (options.onTokenUsage && options.aggregator) {
|
|
9796
|
+
const tokenReport = this.buildTokenReport(options.aggregator);
|
|
9797
|
+
if (tokenReport) {
|
|
9798
|
+
options.onTokenUsage(tokenReport);
|
|
9799
|
+
}
|
|
9800
|
+
}
|
|
9801
|
+
return {
|
|
9802
|
+
...options,
|
|
9803
|
+
ocr_lang: result.detectedLanguages
|
|
9804
|
+
};
|
|
9805
|
+
} finally {
|
|
9806
|
+
if ((0, import_node_fs17.existsSync)(languageDetectionDir)) {
|
|
9807
|
+
(0, import_node_fs17.rmSync)(languageDetectionDir, { recursive: true, force: true });
|
|
9808
|
+
}
|
|
9809
|
+
}
|
|
9810
|
+
}
|
|
9508
9811
|
/**
|
|
9509
9812
|
* Build a token usage report from the aggregator.
|
|
9510
9813
|
* Returns null when no LLM calls were tracked.
|
|
@@ -9663,7 +9966,7 @@ var PDFParser = class {
|
|
|
9663
9966
|
this.baseUrl = void 0;
|
|
9664
9967
|
}
|
|
9665
9968
|
this.timeout = timeout;
|
|
9666
|
-
this.venvPath = venvPath || (0,
|
|
9969
|
+
this.venvPath = venvPath || (0, import_node_path18.join)(process.cwd(), ".venv");
|
|
9667
9970
|
this.killExistingProcess = killExistingProcess;
|
|
9668
9971
|
this.enableImagePdfFallback = enableImagePdfFallback;
|
|
9669
9972
|
}
|
|
@@ -9960,7 +10263,7 @@ var META_DESCRIPTION_PATTERNS_EN = [
|
|
|
9960
10263
|
];
|
|
9961
10264
|
var REPETITIVE_PATTERN_RATIO_THRESHOLD = 0.3;
|
|
9962
10265
|
var REPETITIVE_PATTERN_MIN_REPEATS = 5;
|
|
9963
|
-
var
|
|
10266
|
+
var HANGUL_REGEX2 = /[\uAC00-\uD7AF\u1100-\u11FF]/g;
|
|
9964
10267
|
var CJK_REGEX = /[\u4E00-\u9FFF]/g;
|
|
9965
10268
|
var VlmResponseValidator = class {
|
|
9966
10269
|
/**
|
|
@@ -10028,7 +10331,7 @@ var VlmResponseValidator = class {
|
|
|
10028
10331
|
if (nonWhitespace.length < MIN_CONTENT_LENGTH) {
|
|
10029
10332
|
return null;
|
|
10030
10333
|
}
|
|
10031
|
-
const hangulCount = allContent.match(
|
|
10334
|
+
const hangulCount = allContent.match(HANGUL_REGEX2)?.length ?? 0;
|
|
10032
10335
|
const cjkCount = allContent.match(CJK_REGEX)?.length ?? 0;
|
|
10033
10336
|
const koreanCjkCount = hangulCount + cjkCount;
|
|
10034
10337
|
const ratio = koreanCjkCount / nonWhitespace.length;
|