@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.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/core/pdf-parser.ts
|
|
2
2
|
import { Docling } from "docling-sdk";
|
|
3
|
-
import { join as
|
|
3
|
+
import { join as join17 } from "path";
|
|
4
4
|
|
|
5
5
|
// src/config/constants.ts
|
|
6
6
|
var PDF_PARSER = {
|
|
@@ -44,7 +44,7 @@ var DOCLING_ENVIRONMENT = {
|
|
|
44
44
|
var PAGE_RENDERING = {
|
|
45
45
|
/** Default rendering DPI for VLM text recognition quality */
|
|
46
46
|
DEFAULT_DPI: 200,
|
|
47
|
-
/** Low-resolution DPI for
|
|
47
|
+
/** Low-resolution DPI for language detection sampling */
|
|
48
48
|
SAMPLE_DPI: 150
|
|
49
49
|
};
|
|
50
50
|
var CHUNKED_CONVERSION = {
|
|
@@ -981,17 +981,14 @@ function checkMacOSVersion() {
|
|
|
981
981
|
}
|
|
982
982
|
}
|
|
983
983
|
|
|
984
|
-
// src/
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
}
|
|
993
|
-
name = "ImagePdfFallbackError";
|
|
994
|
-
};
|
|
984
|
+
// src/core/pdf-converter.ts
|
|
985
|
+
import { existsSync as existsSync10, rmSync as rmSync7 } from "fs";
|
|
986
|
+
import { join as join16 } from "path";
|
|
987
|
+
|
|
988
|
+
// src/detectors/pdf-language-detector.ts
|
|
989
|
+
import { normalizeToBcp47 } from "@heripo/model";
|
|
990
|
+
import { readFileSync } from "fs";
|
|
991
|
+
import { z } from "zod/v4";
|
|
995
992
|
|
|
996
993
|
// src/processors/pdf-text-extractor.ts
|
|
997
994
|
var PdfTextExtractor = class _PdfTextExtractor {
|
|
@@ -1122,8 +1119,376 @@ var PdfTextExtractor = class _PdfTextExtractor {
|
|
|
1122
1119
|
}
|
|
1123
1120
|
};
|
|
1124
1121
|
|
|
1122
|
+
// src/prompts/korean-document-detection-prompt.ts
|
|
1123
|
+
var KOREAN_DOCUMENT_DETECTION_PROMPT = `Look at this page image carefully and identify all languages present on this page.
|
|
1124
|
+
|
|
1125
|
+
Return an array of ocrmac-compatible language tags ordered by prevalence (primary language first).
|
|
1126
|
+
Use ko-KR when Korean text is present, even when the page also contains Hanja (\u6F22\u5B57/Chinese characters), English, numbers, tables, or captions.
|
|
1127
|
+
|
|
1128
|
+
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.
|
|
1129
|
+
Examples: ["ko-KR", "en-US"], ["ja-JP"], ["zh-Hant", "en-US"]`;
|
|
1130
|
+
|
|
1131
|
+
// src/detectors/pdf-language-detector.ts
|
|
1132
|
+
var DEFAULT_MAX_SAMPLE_PAGES = 15;
|
|
1133
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
1134
|
+
var EDGE_TRIM_RATIO = 0.1;
|
|
1135
|
+
var HANGUL_REGEX = /[\u1100-\u11ff\u3130-\u318f\uac00-\ud7af]/g;
|
|
1136
|
+
var JAPANESE_KANA_REGEX = /[\u3040-\u30ff]/g;
|
|
1137
|
+
var LATIN_REGEX = /[A-Za-z]/g;
|
|
1138
|
+
var DEFAULT_OCR_LANGUAGES = ["ko-KR", "en-US"];
|
|
1139
|
+
var languageDetectionSchema = z.object({
|
|
1140
|
+
detectedLanguages: z.array(z.string()).describe(
|
|
1141
|
+
"BCP 47 language tags found on this page, ordered by prevalence."
|
|
1142
|
+
)
|
|
1143
|
+
});
|
|
1144
|
+
var PdfLanguageDetector = class {
|
|
1145
|
+
constructor(logger, pageRenderer, textExtractor) {
|
|
1146
|
+
this.logger = logger;
|
|
1147
|
+
this.pageRenderer = pageRenderer;
|
|
1148
|
+
this.textExtractor = textExtractor ?? new PdfTextExtractor(logger);
|
|
1149
|
+
}
|
|
1150
|
+
textExtractor;
|
|
1151
|
+
async detect(pdfPath, outputDir, options) {
|
|
1152
|
+
this.logger.info(
|
|
1153
|
+
"[PdfLanguageDetector] Starting PDF language detection..."
|
|
1154
|
+
);
|
|
1155
|
+
const textLayerResult = await this.detectFromTextLayer(pdfPath);
|
|
1156
|
+
if (textLayerResult) {
|
|
1157
|
+
return textLayerResult;
|
|
1158
|
+
}
|
|
1159
|
+
if (!options?.model) {
|
|
1160
|
+
this.logger.info(
|
|
1161
|
+
`[PdfLanguageDetector] Language detection model not configured; using default OCR languages: ${JSON.stringify(DEFAULT_OCR_LANGUAGES)}`
|
|
1162
|
+
);
|
|
1163
|
+
return {
|
|
1164
|
+
detectedLanguages: [...DEFAULT_OCR_LANGUAGES],
|
|
1165
|
+
reason: "Language detection model not configured",
|
|
1166
|
+
sampledPages: 0,
|
|
1167
|
+
totalPages: 0,
|
|
1168
|
+
source: "default"
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
return this.detectFromSampledPages(pdfPath, outputDir, {
|
|
1172
|
+
...options,
|
|
1173
|
+
model: options.model
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
async detectFromTextLayer(pdfPath) {
|
|
1177
|
+
try {
|
|
1178
|
+
const totalPages = await this.textExtractor.getPageCount(pdfPath);
|
|
1179
|
+
if (totalPages === 0) return null;
|
|
1180
|
+
const fullText = await this.textExtractor.extractFullText(pdfPath);
|
|
1181
|
+
if (fullText.trim().length === 0) {
|
|
1182
|
+
this.logger.debug(
|
|
1183
|
+
"[PdfLanguageDetector] No text in text layer; falling back to sampled language detection"
|
|
1184
|
+
);
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
const detectedLanguages = this.detectLanguagesFromText(fullText);
|
|
1188
|
+
if (detectedLanguages.length === 0) {
|
|
1189
|
+
this.logger.debug(
|
|
1190
|
+
"[PdfLanguageDetector] No known language signal in text layer; falling back to sampled language detection"
|
|
1191
|
+
);
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
this.logger.info(
|
|
1195
|
+
`[PdfLanguageDetector] Languages detected in text layer: ${detectedLanguages.join(", ")}`
|
|
1196
|
+
);
|
|
1197
|
+
return {
|
|
1198
|
+
detectedLanguages,
|
|
1199
|
+
reason: `Languages found in PDF text layer (${totalPages} pages checked)`,
|
|
1200
|
+
sampledPages: totalPages,
|
|
1201
|
+
totalPages,
|
|
1202
|
+
source: "text-layer"
|
|
1203
|
+
};
|
|
1204
|
+
} catch {
|
|
1205
|
+
this.logger.debug(
|
|
1206
|
+
"[PdfLanguageDetector] Text layer language detection failed; falling back to sampled language detection"
|
|
1207
|
+
);
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
detectLanguagesFromText(text) {
|
|
1212
|
+
const scores = [
|
|
1213
|
+
{ tag: "ko-KR", count: this.countMatches(text, HANGUL_REGEX) },
|
|
1214
|
+
{ tag: "ja-JP", count: this.countMatches(text, JAPANESE_KANA_REGEX) },
|
|
1215
|
+
{ tag: "en-US", count: this.countMatches(text, LATIN_REGEX) }
|
|
1216
|
+
];
|
|
1217
|
+
return scores.filter(({ count }) => count > 0).sort((a, b) => b.count - a.count).map(({ tag }) => tag);
|
|
1218
|
+
}
|
|
1219
|
+
countMatches(text, regex) {
|
|
1220
|
+
regex.lastIndex = 0;
|
|
1221
|
+
return text.match(regex)?.length ?? 0;
|
|
1222
|
+
}
|
|
1223
|
+
async detectFromSampledPages(pdfPath, outputDir, options) {
|
|
1224
|
+
const renderResult = await this.pageRenderer.renderPages(
|
|
1225
|
+
pdfPath,
|
|
1226
|
+
outputDir,
|
|
1227
|
+
{
|
|
1228
|
+
dpi: PAGE_RENDERING.SAMPLE_DPI
|
|
1229
|
+
}
|
|
1230
|
+
);
|
|
1231
|
+
if (renderResult.pageCount === 0) {
|
|
1232
|
+
this.logger.info(
|
|
1233
|
+
`[PdfLanguageDetector] No pages found; using default OCR languages: ${JSON.stringify(DEFAULT_OCR_LANGUAGES)}`
|
|
1234
|
+
);
|
|
1235
|
+
return {
|
|
1236
|
+
detectedLanguages: [...DEFAULT_OCR_LANGUAGES],
|
|
1237
|
+
reason: "No pages found in PDF",
|
|
1238
|
+
sampledPages: 0,
|
|
1239
|
+
totalPages: 0,
|
|
1240
|
+
source: "default"
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
const sampleIndices = this.selectSamplePages(
|
|
1244
|
+
renderResult.pageCount,
|
|
1245
|
+
options.maxSamplePages ?? DEFAULT_MAX_SAMPLE_PAGES
|
|
1246
|
+
);
|
|
1247
|
+
this.logger.info(
|
|
1248
|
+
`[PdfLanguageDetector] Sampling ${sampleIndices.length} of ${renderResult.pageCount} pages for language detection: [${sampleIndices.map((i) => i + 1).join(", ")}]`
|
|
1249
|
+
);
|
|
1250
|
+
const languageFrequency = /* @__PURE__ */ new Map();
|
|
1251
|
+
let sampledPages = 0;
|
|
1252
|
+
for (const index of sampleIndices) {
|
|
1253
|
+
const pageFile = renderResult.pageFiles[index];
|
|
1254
|
+
if (!pageFile) continue;
|
|
1255
|
+
sampledPages += 1;
|
|
1256
|
+
const detectedLanguages2 = await this.analyzeSamplePage(
|
|
1257
|
+
pageFile,
|
|
1258
|
+
index + 1,
|
|
1259
|
+
options
|
|
1260
|
+
);
|
|
1261
|
+
for (const language of detectedLanguages2) {
|
|
1262
|
+
languageFrequency.set(
|
|
1263
|
+
language,
|
|
1264
|
+
(languageFrequency.get(language) ?? 0) + 1
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const detectedLanguages = this.aggregateLanguages(languageFrequency);
|
|
1269
|
+
if (detectedLanguages.length === 0) {
|
|
1270
|
+
this.logger.info(
|
|
1271
|
+
`[PdfLanguageDetector] No languages detected from sampled pages; using default OCR languages: ${JSON.stringify(DEFAULT_OCR_LANGUAGES)}`
|
|
1272
|
+
);
|
|
1273
|
+
return {
|
|
1274
|
+
detectedLanguages: [...DEFAULT_OCR_LANGUAGES],
|
|
1275
|
+
reason: `No language detected in ${sampledPages} sampled pages`,
|
|
1276
|
+
sampledPages,
|
|
1277
|
+
totalPages: renderResult.pageCount,
|
|
1278
|
+
source: "default"
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
this.logger.info(
|
|
1282
|
+
`[PdfLanguageDetector] Languages detected from sampled pages: ${detectedLanguages.join(", ")}`
|
|
1283
|
+
);
|
|
1284
|
+
return {
|
|
1285
|
+
detectedLanguages,
|
|
1286
|
+
reason: `Languages detected in ${sampledPages} sampled pages`,
|
|
1287
|
+
sampledPages,
|
|
1288
|
+
totalPages: renderResult.pageCount,
|
|
1289
|
+
source: "vlm"
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
selectSamplePages(totalPages, maxSamples) {
|
|
1293
|
+
if (totalPages === 0) return [];
|
|
1294
|
+
if (totalPages <= maxSamples) {
|
|
1295
|
+
return Array.from({ length: totalPages }, (_, i) => i);
|
|
1296
|
+
}
|
|
1297
|
+
const trimCount = Math.max(1, Math.ceil(totalPages * EDGE_TRIM_RATIO));
|
|
1298
|
+
const start = trimCount;
|
|
1299
|
+
const end = totalPages - trimCount;
|
|
1300
|
+
const eligibleCount = end - start;
|
|
1301
|
+
if (eligibleCount <= 0) {
|
|
1302
|
+
return [Math.floor(totalPages / 2)];
|
|
1303
|
+
}
|
|
1304
|
+
if (eligibleCount <= maxSamples) {
|
|
1305
|
+
return Array.from({ length: eligibleCount }, (_, i) => start + i);
|
|
1306
|
+
}
|
|
1307
|
+
const step = eligibleCount / maxSamples;
|
|
1308
|
+
return Array.from(
|
|
1309
|
+
{ length: maxSamples },
|
|
1310
|
+
(_, i) => start + Math.floor(i * step)
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
async analyzeSamplePage(pageFile, pageNo, options) {
|
|
1314
|
+
this.logger.debug(
|
|
1315
|
+
`[PdfLanguageDetector] Analyzing page ${pageNo} for language...`
|
|
1316
|
+
);
|
|
1317
|
+
const imageData = new Uint8Array(readFileSync(pageFile));
|
|
1318
|
+
const result = await LLMCaller.callVision({
|
|
1319
|
+
schema: languageDetectionSchema,
|
|
1320
|
+
messages: [
|
|
1321
|
+
{
|
|
1322
|
+
role: "user",
|
|
1323
|
+
content: [
|
|
1324
|
+
{
|
|
1325
|
+
type: "text",
|
|
1326
|
+
text: KOREAN_DOCUMENT_DETECTION_PROMPT
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
type: "image",
|
|
1330
|
+
image: imageData,
|
|
1331
|
+
mediaType: "image/png"
|
|
1332
|
+
}
|
|
1333
|
+
]
|
|
1334
|
+
}
|
|
1335
|
+
],
|
|
1336
|
+
primaryModel: options.model,
|
|
1337
|
+
maxRetries: options.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
1338
|
+
temperature: options.temperature ?? 0,
|
|
1339
|
+
abortSignal: options.abortSignal,
|
|
1340
|
+
component: "PdfLanguageDetector",
|
|
1341
|
+
phase: "language-detection"
|
|
1342
|
+
});
|
|
1343
|
+
options.aggregator?.track(result.usage);
|
|
1344
|
+
const output = result.output;
|
|
1345
|
+
const detectedLanguages = output.detectedLanguages.map(normalizeToBcp47).filter((tag) => tag !== null);
|
|
1346
|
+
this.logger.debug(
|
|
1347
|
+
`[PdfLanguageDetector] Page ${pageNo}: detectedLanguages=${detectedLanguages.join(",")}`
|
|
1348
|
+
);
|
|
1349
|
+
return detectedLanguages;
|
|
1350
|
+
}
|
|
1351
|
+
aggregateLanguages(languageFrequency) {
|
|
1352
|
+
return [...languageFrequency.entries()].sort((a, b) => b[1] - a[1]).map(([language]) => language);
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// src/errors/image-pdf-fallback-error.ts
|
|
1357
|
+
var ImagePdfFallbackError = class extends Error {
|
|
1358
|
+
constructor(originalError, fallbackError) {
|
|
1359
|
+
super(
|
|
1360
|
+
`PDF conversion failed with fallback. Original: ${originalError.message}. Fallback: ${fallbackError.message}`
|
|
1361
|
+
);
|
|
1362
|
+
this.originalError = originalError;
|
|
1363
|
+
this.fallbackError = fallbackError;
|
|
1364
|
+
}
|
|
1365
|
+
name = "ImagePdfFallbackError";
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
// src/processors/page-renderer.ts
|
|
1369
|
+
import { existsSync, mkdirSync, readdirSync } from "fs";
|
|
1370
|
+
import { join as join3 } from "path";
|
|
1371
|
+
var PROGRESS_LOG_PERCENT_STEP = 10;
|
|
1372
|
+
var PageRenderer = class {
|
|
1373
|
+
constructor(logger) {
|
|
1374
|
+
this.logger = logger;
|
|
1375
|
+
}
|
|
1376
|
+
lastLoggedPercent = 0;
|
|
1377
|
+
/**
|
|
1378
|
+
* Render all pages of a PDF to individual PNG files.
|
|
1379
|
+
*
|
|
1380
|
+
* Uses per-page rendering (`magick 'input.pdf[N]'`) when page count is known,
|
|
1381
|
+
* limiting peak memory to ~15MB/page instead of loading all pages at once.
|
|
1382
|
+
*
|
|
1383
|
+
* @param pdfPath - Absolute path to the source PDF file
|
|
1384
|
+
* @param outputDir - Directory where pages/ subdirectory will be created
|
|
1385
|
+
* @param options - Rendering options
|
|
1386
|
+
* @returns Render result with page count and file paths
|
|
1387
|
+
*/
|
|
1388
|
+
async renderPages(pdfPath, outputDir, options) {
|
|
1389
|
+
const dpi = options?.dpi ?? PAGE_RENDERING.DEFAULT_DPI;
|
|
1390
|
+
const pagesDir = join3(outputDir, "pages");
|
|
1391
|
+
if (!existsSync(pagesDir)) {
|
|
1392
|
+
mkdirSync(pagesDir, { recursive: true });
|
|
1393
|
+
}
|
|
1394
|
+
const totalPages = await this.getPageCount(pdfPath);
|
|
1395
|
+
if (totalPages > 0) {
|
|
1396
|
+
this.logger.info(
|
|
1397
|
+
`[PageRenderer] Rendering ${totalPages} pages at ${dpi} DPI...`
|
|
1398
|
+
);
|
|
1399
|
+
this.lastLoggedPercent = 0;
|
|
1400
|
+
for (let i = 0; i < totalPages; i++) {
|
|
1401
|
+
const result = await spawnAsync(
|
|
1402
|
+
"magick",
|
|
1403
|
+
[
|
|
1404
|
+
"-density",
|
|
1405
|
+
dpi.toString(),
|
|
1406
|
+
`${pdfPath}[${i}]`,
|
|
1407
|
+
"-background",
|
|
1408
|
+
"white",
|
|
1409
|
+
"-alpha",
|
|
1410
|
+
"remove",
|
|
1411
|
+
"-alpha",
|
|
1412
|
+
"off",
|
|
1413
|
+
join3(pagesDir, `page_${i}.png`)
|
|
1414
|
+
],
|
|
1415
|
+
{ captureStdout: false }
|
|
1416
|
+
);
|
|
1417
|
+
if (result.code !== 0) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
`[PageRenderer] Failed to render page ${i + 1}/${totalPages}: ${result.stderr || "Unknown error"}`
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
this.logProgress(i + 1, totalPages);
|
|
1423
|
+
}
|
|
1424
|
+
} else {
|
|
1425
|
+
this.logger.info(`[PageRenderer] Rendering PDF at ${dpi} DPI...`);
|
|
1426
|
+
const result = await spawnAsync(
|
|
1427
|
+
"magick",
|
|
1428
|
+
[
|
|
1429
|
+
"-density",
|
|
1430
|
+
dpi.toString(),
|
|
1431
|
+
pdfPath,
|
|
1432
|
+
"-background",
|
|
1433
|
+
"white",
|
|
1434
|
+
"-alpha",
|
|
1435
|
+
"remove",
|
|
1436
|
+
"-alpha",
|
|
1437
|
+
"off",
|
|
1438
|
+
join3(pagesDir, "page_%d.png")
|
|
1439
|
+
],
|
|
1440
|
+
{ captureStdout: false }
|
|
1441
|
+
);
|
|
1442
|
+
if (result.code !== 0) {
|
|
1443
|
+
throw new Error(
|
|
1444
|
+
`[PageRenderer] Failed to render PDF pages: ${result.stderr || "Unknown error"}`
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const pageFiles = readdirSync(pagesDir).filter((f) => f.startsWith("page_") && f.endsWith(".png")).sort((a, b) => {
|
|
1449
|
+
const numA = parseInt(a.replace("page_", "").replace(".png", ""), 10);
|
|
1450
|
+
const numB = parseInt(b.replace("page_", "").replace(".png", ""), 10);
|
|
1451
|
+
return numA - numB;
|
|
1452
|
+
}).map((f) => join3(pagesDir, f));
|
|
1453
|
+
this.logger.info(
|
|
1454
|
+
`[PageRenderer] Rendered ${pageFiles.length} pages to ${pagesDir}`
|
|
1455
|
+
);
|
|
1456
|
+
return {
|
|
1457
|
+
pageCount: pageFiles.length,
|
|
1458
|
+
pagesDir,
|
|
1459
|
+
pageFiles
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Log rendering progress at appropriate intervals (every 10%).
|
|
1464
|
+
*/
|
|
1465
|
+
logProgress(current, total) {
|
|
1466
|
+
const percent = Math.floor(current / total * 100);
|
|
1467
|
+
if (percent >= this.lastLoggedPercent + PROGRESS_LOG_PERCENT_STEP || current === total) {
|
|
1468
|
+
this.lastLoggedPercent = percent;
|
|
1469
|
+
this.logger.info(
|
|
1470
|
+
`[PageRenderer] Rendering pages: ${current}/${total} (${percent}%)`
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Get total page count using pdfinfo.
|
|
1476
|
+
* Returns 0 on failure (progress logging will be skipped).
|
|
1477
|
+
*/
|
|
1478
|
+
async getPageCount(pdfPath) {
|
|
1479
|
+
try {
|
|
1480
|
+
const result = await spawnAsync("pdfinfo", [pdfPath]);
|
|
1481
|
+
if (result.code !== 0) return 0;
|
|
1482
|
+
const match = result.stdout.match(/^Pages:\s+(\d+)/m);
|
|
1483
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
1484
|
+
} catch {
|
|
1485
|
+
return 0;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
|
|
1125
1490
|
// src/validators/document-type-validator.ts
|
|
1126
|
-
import { z } from "zod";
|
|
1491
|
+
import { z as z2 } from "zod";
|
|
1127
1492
|
|
|
1128
1493
|
// src/errors/invalid-document-type-error.ts
|
|
1129
1494
|
var InvalidDocumentTypeError = class extends Error {
|
|
@@ -1162,9 +1527,9 @@ NOT valid (these are NOT archaeological investigation reports):
|
|
|
1162
1527
|
- Museum catalogs or exhibition guides`;
|
|
1163
1528
|
|
|
1164
1529
|
// src/validators/document-type-validator.ts
|
|
1165
|
-
var documentTypeSchema =
|
|
1166
|
-
isValid:
|
|
1167
|
-
reason:
|
|
1530
|
+
var documentTypeSchema = z2.object({
|
|
1531
|
+
isValid: z2.boolean().describe("Whether this is an archaeological investigation report"),
|
|
1532
|
+
reason: z2.string().describe("Brief reason for the decision")
|
|
1168
1533
|
});
|
|
1169
1534
|
var DocumentTypeValidator = class {
|
|
1170
1535
|
textExtractor;
|
|
@@ -1217,7 +1582,7 @@ import {
|
|
|
1217
1582
|
copyFileSync,
|
|
1218
1583
|
existsSync as existsSync3,
|
|
1219
1584
|
mkdirSync as mkdirSync3,
|
|
1220
|
-
readFileSync,
|
|
1585
|
+
readFileSync as readFileSync2,
|
|
1221
1586
|
readdirSync as readdirSync3,
|
|
1222
1587
|
rmSync as rmSync2,
|
|
1223
1588
|
writeFileSync as writeFileSync2
|
|
@@ -1357,13 +1722,13 @@ var DoclingDocumentMerger = class {
|
|
|
1357
1722
|
import {
|
|
1358
1723
|
createReadStream,
|
|
1359
1724
|
createWriteStream as createWriteStream2,
|
|
1360
|
-
existsSync,
|
|
1361
|
-
mkdirSync,
|
|
1362
|
-
readdirSync,
|
|
1725
|
+
existsSync as existsSync2,
|
|
1726
|
+
mkdirSync as mkdirSync2,
|
|
1727
|
+
readdirSync as readdirSync2,
|
|
1363
1728
|
rmSync,
|
|
1364
1729
|
writeFileSync
|
|
1365
1730
|
} from "fs";
|
|
1366
|
-
import { extname, join as
|
|
1731
|
+
import { extname, join as join4 } from "path";
|
|
1367
1732
|
import { Transform } from "stream";
|
|
1368
1733
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
1369
1734
|
import * as yauzl from "yauzl";
|
|
@@ -1575,9 +1940,9 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1575
1940
|
}
|
|
1576
1941
|
zipfile.readEntry();
|
|
1577
1942
|
zipfile.on("entry", (entry) => {
|
|
1578
|
-
const entryPath =
|
|
1943
|
+
const entryPath = join4(targetDir, entry.fileName);
|
|
1579
1944
|
if (/\/$/.test(entry.fileName)) {
|
|
1580
|
-
|
|
1945
|
+
mkdirSync2(entryPath, { recursive: true });
|
|
1581
1946
|
zipfile.readEntry();
|
|
1582
1947
|
} else {
|
|
1583
1948
|
zipfile.openReadStream(entry, (err2, readStream) => {
|
|
@@ -1585,7 +1950,7 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1585
1950
|
reject(err2 || new Error("Failed to open read stream"));
|
|
1586
1951
|
return;
|
|
1587
1952
|
}
|
|
1588
|
-
|
|
1953
|
+
mkdirSync2(join4(entryPath, ".."), { recursive: true });
|
|
1589
1954
|
const writeStream = createWriteStream2(entryPath);
|
|
1590
1955
|
readStream.pipe(writeStream);
|
|
1591
1956
|
writeStream.on("finish", () => {
|
|
@@ -1609,7 +1974,7 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1609
1974
|
const PREFIX = "data:image/png;base64,";
|
|
1610
1975
|
const base64Content = base64Data.startsWith(PREFIX) ? base64Data.slice(PREFIX.length) : base64Data;
|
|
1611
1976
|
const filename = `${prefix}_${index}.png`;
|
|
1612
|
-
const filepath =
|
|
1977
|
+
const filepath = join4(imagesDir, filename);
|
|
1613
1978
|
const buffer = Buffer.from(base64Content, "base64");
|
|
1614
1979
|
writeFileSync(filepath, buffer);
|
|
1615
1980
|
return `${dirName}/${filename}`;
|
|
@@ -1643,7 +2008,7 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1643
2008
|
}
|
|
1644
2009
|
const base64Content = pending.slice(dataStart, quoteIdx);
|
|
1645
2010
|
const filename = `image_${imageIndex}.png`;
|
|
1646
|
-
const filepath =
|
|
2011
|
+
const filepath = join4(imagesDir, filename);
|
|
1647
2012
|
const buf = Buffer.from(base64Content, "base64");
|
|
1648
2013
|
writeFileSync(filepath, buf);
|
|
1649
2014
|
const relativePath = `images/${filename}`;
|
|
@@ -1687,19 +2052,19 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1687
2052
|
*/
|
|
1688
2053
|
static async saveDocumentsWithExtractedImages(logger, outputDir, filename, jsonSourcePath, htmlSourcePath) {
|
|
1689
2054
|
try {
|
|
1690
|
-
if (
|
|
2055
|
+
if (existsSync2(outputDir)) {
|
|
1691
2056
|
rmSync(outputDir, { recursive: true, force: true });
|
|
1692
2057
|
}
|
|
1693
2058
|
} catch (e) {
|
|
1694
2059
|
logger.warn("[PDFConverter] Failed to clear output directory:", e);
|
|
1695
2060
|
}
|
|
1696
|
-
|
|
2061
|
+
mkdirSync2(outputDir, { recursive: true });
|
|
1697
2062
|
const baseName = filename.replace(extname(filename), "");
|
|
1698
|
-
const jsonPath =
|
|
2063
|
+
const jsonPath = join4(outputDir, `${baseName}.json`);
|
|
1699
2064
|
try {
|
|
1700
|
-
const imagesDir =
|
|
1701
|
-
if (!
|
|
1702
|
-
|
|
2065
|
+
const imagesDir = join4(outputDir, "images");
|
|
2066
|
+
if (!existsSync2(imagesDir)) {
|
|
2067
|
+
mkdirSync2(imagesDir, { recursive: true });
|
|
1703
2068
|
}
|
|
1704
2069
|
const imageCount = await jqExtractBase64PngStringsStreaming(
|
|
1705
2070
|
jsonSourcePath,
|
|
@@ -1733,11 +2098,11 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1733
2098
|
throw e;
|
|
1734
2099
|
}
|
|
1735
2100
|
logger.info("[PDFConverter] Saved JSON:", jsonPath);
|
|
1736
|
-
const htmlPath =
|
|
2101
|
+
const htmlPath = join4(outputDir, `${baseName}.html`);
|
|
1737
2102
|
try {
|
|
1738
|
-
const imagesDir =
|
|
1739
|
-
if (!
|
|
1740
|
-
|
|
2103
|
+
const imagesDir = join4(outputDir, "images");
|
|
2104
|
+
if (!existsSync2(imagesDir)) {
|
|
2105
|
+
mkdirSync2(imagesDir, { recursive: true });
|
|
1741
2106
|
}
|
|
1742
2107
|
const htmlImageCount = await _ImageExtractor.extractImagesFromHtmlStream(
|
|
1743
2108
|
htmlSourcePath,
|
|
@@ -1774,7 +2139,7 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1774
2139
|
static async extractAndSaveDocumentsFromZip(logger, zipPath, extractDir, outputDir) {
|
|
1775
2140
|
logger.info("[PDFConverter] Extracting ZIP file...");
|
|
1776
2141
|
await _ImageExtractor.extractZip(zipPath, extractDir);
|
|
1777
|
-
const files =
|
|
2142
|
+
const files = readdirSync2(extractDir);
|
|
1778
2143
|
const jsonFile = files.find((f) => extname(f).toLowerCase() === ".json");
|
|
1779
2144
|
const htmlFile = files.find((f) => extname(f).toLowerCase() === ".html");
|
|
1780
2145
|
if (!jsonFile || !htmlFile) {
|
|
@@ -1782,8 +2147,8 @@ var ImageExtractor = class _ImageExtractor {
|
|
|
1782
2147
|
`Expected one JSON and one HTML file in extracted directory. Found: ${files.join(", ")}`
|
|
1783
2148
|
);
|
|
1784
2149
|
}
|
|
1785
|
-
const jsonPath =
|
|
1786
|
-
const htmlPath =
|
|
2150
|
+
const jsonPath = join4(extractDir, jsonFile);
|
|
2151
|
+
const htmlPath = join4(extractDir, htmlFile);
|
|
1787
2152
|
logger.info("[PDFConverter] Saving converted files to output...");
|
|
1788
2153
|
await _ImageExtractor.saveDocumentsWithExtractedImages(
|
|
1789
2154
|
logger,
|
|
@@ -1892,130 +2257,6 @@ var LocalFileServer = class {
|
|
|
1892
2257
|
// src/utils/page-image-updater.ts
|
|
1893
2258
|
import { rename as rename2 } from "fs/promises";
|
|
1894
2259
|
import { join as join5 } from "path";
|
|
1895
|
-
|
|
1896
|
-
// src/processors/page-renderer.ts
|
|
1897
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "fs";
|
|
1898
|
-
import { join as join4 } from "path";
|
|
1899
|
-
var PROGRESS_LOG_PERCENT_STEP = 10;
|
|
1900
|
-
var PageRenderer = class {
|
|
1901
|
-
constructor(logger) {
|
|
1902
|
-
this.logger = logger;
|
|
1903
|
-
}
|
|
1904
|
-
lastLoggedPercent = 0;
|
|
1905
|
-
/**
|
|
1906
|
-
* Render all pages of a PDF to individual PNG files.
|
|
1907
|
-
*
|
|
1908
|
-
* Uses per-page rendering (`magick 'input.pdf[N]'`) when page count is known,
|
|
1909
|
-
* limiting peak memory to ~15MB/page instead of loading all pages at once.
|
|
1910
|
-
*
|
|
1911
|
-
* @param pdfPath - Absolute path to the source PDF file
|
|
1912
|
-
* @param outputDir - Directory where pages/ subdirectory will be created
|
|
1913
|
-
* @param options - Rendering options
|
|
1914
|
-
* @returns Render result with page count and file paths
|
|
1915
|
-
*/
|
|
1916
|
-
async renderPages(pdfPath, outputDir, options) {
|
|
1917
|
-
const dpi = options?.dpi ?? PAGE_RENDERING.DEFAULT_DPI;
|
|
1918
|
-
const pagesDir = join4(outputDir, "pages");
|
|
1919
|
-
if (!existsSync2(pagesDir)) {
|
|
1920
|
-
mkdirSync2(pagesDir, { recursive: true });
|
|
1921
|
-
}
|
|
1922
|
-
const totalPages = await this.getPageCount(pdfPath);
|
|
1923
|
-
if (totalPages > 0) {
|
|
1924
|
-
this.logger.info(
|
|
1925
|
-
`[PageRenderer] Rendering ${totalPages} pages at ${dpi} DPI...`
|
|
1926
|
-
);
|
|
1927
|
-
this.lastLoggedPercent = 0;
|
|
1928
|
-
for (let i = 0; i < totalPages; i++) {
|
|
1929
|
-
const result = await spawnAsync(
|
|
1930
|
-
"magick",
|
|
1931
|
-
[
|
|
1932
|
-
"-density",
|
|
1933
|
-
dpi.toString(),
|
|
1934
|
-
`${pdfPath}[${i}]`,
|
|
1935
|
-
"-background",
|
|
1936
|
-
"white",
|
|
1937
|
-
"-alpha",
|
|
1938
|
-
"remove",
|
|
1939
|
-
"-alpha",
|
|
1940
|
-
"off",
|
|
1941
|
-
join4(pagesDir, `page_${i}.png`)
|
|
1942
|
-
],
|
|
1943
|
-
{ captureStdout: false }
|
|
1944
|
-
);
|
|
1945
|
-
if (result.code !== 0) {
|
|
1946
|
-
throw new Error(
|
|
1947
|
-
`[PageRenderer] Failed to render page ${i + 1}/${totalPages}: ${result.stderr || "Unknown error"}`
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
this.logProgress(i + 1, totalPages);
|
|
1951
|
-
}
|
|
1952
|
-
} else {
|
|
1953
|
-
this.logger.info(`[PageRenderer] Rendering PDF at ${dpi} DPI...`);
|
|
1954
|
-
const result = await spawnAsync(
|
|
1955
|
-
"magick",
|
|
1956
|
-
[
|
|
1957
|
-
"-density",
|
|
1958
|
-
dpi.toString(),
|
|
1959
|
-
pdfPath,
|
|
1960
|
-
"-background",
|
|
1961
|
-
"white",
|
|
1962
|
-
"-alpha",
|
|
1963
|
-
"remove",
|
|
1964
|
-
"-alpha",
|
|
1965
|
-
"off",
|
|
1966
|
-
join4(pagesDir, "page_%d.png")
|
|
1967
|
-
],
|
|
1968
|
-
{ captureStdout: false }
|
|
1969
|
-
);
|
|
1970
|
-
if (result.code !== 0) {
|
|
1971
|
-
throw new Error(
|
|
1972
|
-
`[PageRenderer] Failed to render PDF pages: ${result.stderr || "Unknown error"}`
|
|
1973
|
-
);
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
|
-
const pageFiles = readdirSync2(pagesDir).filter((f) => f.startsWith("page_") && f.endsWith(".png")).sort((a, b) => {
|
|
1977
|
-
const numA = parseInt(a.replace("page_", "").replace(".png", ""), 10);
|
|
1978
|
-
const numB = parseInt(b.replace("page_", "").replace(".png", ""), 10);
|
|
1979
|
-
return numA - numB;
|
|
1980
|
-
}).map((f) => join4(pagesDir, f));
|
|
1981
|
-
this.logger.info(
|
|
1982
|
-
`[PageRenderer] Rendered ${pageFiles.length} pages to ${pagesDir}`
|
|
1983
|
-
);
|
|
1984
|
-
return {
|
|
1985
|
-
pageCount: pageFiles.length,
|
|
1986
|
-
pagesDir,
|
|
1987
|
-
pageFiles
|
|
1988
|
-
};
|
|
1989
|
-
}
|
|
1990
|
-
/**
|
|
1991
|
-
* Log rendering progress at appropriate intervals (every 10%).
|
|
1992
|
-
*/
|
|
1993
|
-
logProgress(current, total) {
|
|
1994
|
-
const percent = Math.floor(current / total * 100);
|
|
1995
|
-
if (percent >= this.lastLoggedPercent + PROGRESS_LOG_PERCENT_STEP || current === total) {
|
|
1996
|
-
this.lastLoggedPercent = percent;
|
|
1997
|
-
this.logger.info(
|
|
1998
|
-
`[PageRenderer] Rendering pages: ${current}/${total} (${percent}%)`
|
|
1999
|
-
);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
/**
|
|
2003
|
-
* Get total page count using pdfinfo.
|
|
2004
|
-
* Returns 0 on failure (progress logging will be skipped).
|
|
2005
|
-
*/
|
|
2006
|
-
async getPageCount(pdfPath) {
|
|
2007
|
-
try {
|
|
2008
|
-
const result = await spawnAsync("pdfinfo", [pdfPath]);
|
|
2009
|
-
if (result.code !== 0) return 0;
|
|
2010
|
-
const match = result.stdout.match(/^Pages:\s+(\d+)/m);
|
|
2011
|
-
return match ? parseInt(match[1], 10) : 0;
|
|
2012
|
-
} catch {
|
|
2013
|
-
return 0;
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
};
|
|
2017
|
-
|
|
2018
|
-
// src/utils/page-image-updater.ts
|
|
2019
2260
|
async function renderAndUpdatePageImages(pdfPath, outputDir, logger, logPrefix) {
|
|
2020
2261
|
logger.info(`${logPrefix} Rendering page images with ImageMagick...`);
|
|
2021
2262
|
const renderer = new PageRenderer(logger);
|
|
@@ -2127,6 +2368,7 @@ ${logPrefix} Task failed after ${elapsed}s: ${errorDetails}`
|
|
|
2127
2368
|
// src/core/conversion-options-builder.ts
|
|
2128
2369
|
import { omit } from "es-toolkit";
|
|
2129
2370
|
function buildConversionOptions(options) {
|
|
2371
|
+
const ocrLanguages = options.ocr_lang ?? DEFAULT_OCR_LANGUAGES;
|
|
2130
2372
|
return {
|
|
2131
2373
|
...omit(options, [
|
|
2132
2374
|
"num_threads",
|
|
@@ -2139,14 +2381,18 @@ function buildConversionOptions(options) {
|
|
|
2139
2381
|
"chunkedConversion",
|
|
2140
2382
|
"chunkSize",
|
|
2141
2383
|
"chunkMaxRetries",
|
|
2142
|
-
"documentValidationModel"
|
|
2384
|
+
"documentValidationModel",
|
|
2385
|
+
"languageDetectionModel"
|
|
2143
2386
|
]),
|
|
2144
2387
|
to_formats: ["json", "html"],
|
|
2145
2388
|
image_export_mode: "embedded",
|
|
2389
|
+
// Docling currently reads this top-level field in addition to ocr_options.
|
|
2390
|
+
// Keep it populated even when language detection is skipped.
|
|
2391
|
+
ocr_lang: ocrLanguages,
|
|
2146
2392
|
ocr_engine: "ocrmac",
|
|
2147
2393
|
ocr_options: {
|
|
2148
2394
|
kind: "ocrmac",
|
|
2149
|
-
lang:
|
|
2395
|
+
lang: ocrLanguages,
|
|
2150
2396
|
recognition: "accurate",
|
|
2151
2397
|
framework: "livetext"
|
|
2152
2398
|
},
|
|
@@ -2473,7 +2719,7 @@ var ChunkedPDFConverter = class {
|
|
|
2473
2719
|
* pages/page_N.png, these pic_ files become orphaned.
|
|
2474
2720
|
*/
|
|
2475
2721
|
cleanupOrphanedPicFiles(resultPath, imagesDir) {
|
|
2476
|
-
const content =
|
|
2722
|
+
const content = readFileSync2(resultPath, "utf-8");
|
|
2477
2723
|
const referencedPics = new Set(
|
|
2478
2724
|
[...content.matchAll(/images\/pic_\d+\.png/g)].map(
|
|
2479
2725
|
(m) => m[0].replace("images/", "")
|
|
@@ -2775,7 +3021,7 @@ import { join as join15 } from "path";
|
|
|
2775
3021
|
|
|
2776
3022
|
// src/processors/post-docling-page-processor.ts
|
|
2777
3023
|
import { buildLanguageDescription } from "@heripo/model";
|
|
2778
|
-
import { readFileSync as
|
|
3024
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
2779
3025
|
import { join as join10 } from "path";
|
|
2780
3026
|
|
|
2781
3027
|
// src/prompts/text-correction-prompt.ts
|
|
@@ -2832,35 +3078,35 @@ Substitution rules for tc:
|
|
|
2832
3078
|
If all correct: {"tc":[],"cc":[]}`;
|
|
2833
3079
|
|
|
2834
3080
|
// src/types/vlm-text-correction-schema.ts
|
|
2835
|
-
import { z as
|
|
2836
|
-
var vlmTextCorrectionSchema =
|
|
3081
|
+
import { z as z3 } from "zod/v4";
|
|
3082
|
+
var vlmTextCorrectionSchema = z3.object({
|
|
2837
3083
|
/** Text element corrections (substitution-based) */
|
|
2838
|
-
tc:
|
|
2839
|
-
|
|
3084
|
+
tc: z3.array(
|
|
3085
|
+
z3.object({
|
|
2840
3086
|
/** Text element index (from prompt) */
|
|
2841
|
-
i:
|
|
3087
|
+
i: z3.number().int().nonnegative(),
|
|
2842
3088
|
/** Substitutions: find/replace pairs applied left-to-right */
|
|
2843
|
-
s:
|
|
2844
|
-
|
|
3089
|
+
s: z3.array(
|
|
3090
|
+
z3.object({
|
|
2845
3091
|
/** Exact garbled substring to find */
|
|
2846
|
-
f:
|
|
3092
|
+
f: z3.string(),
|
|
2847
3093
|
/** Corrected replacement text */
|
|
2848
|
-
r:
|
|
3094
|
+
r: z3.string()
|
|
2849
3095
|
})
|
|
2850
3096
|
)
|
|
2851
3097
|
})
|
|
2852
3098
|
),
|
|
2853
3099
|
/** Table cell corrections */
|
|
2854
|
-
cc:
|
|
2855
|
-
|
|
3100
|
+
cc: z3.array(
|
|
3101
|
+
z3.object({
|
|
2856
3102
|
/** Table index (within the page) */
|
|
2857
|
-
ti:
|
|
3103
|
+
ti: z3.number().int().nonnegative(),
|
|
2858
3104
|
/** Row index */
|
|
2859
|
-
r:
|
|
3105
|
+
r: z3.number().int().nonnegative(),
|
|
2860
3106
|
/** Column index */
|
|
2861
|
-
c:
|
|
3107
|
+
c: z3.number().int().nonnegative(),
|
|
2862
3108
|
/** Corrected cell text */
|
|
2863
|
-
t:
|
|
3109
|
+
t: z3.string()
|
|
2864
3110
|
})
|
|
2865
3111
|
)
|
|
2866
3112
|
});
|
|
@@ -2998,21 +3244,21 @@ function applyCorrections(doc, pageNo, corrections, logger) {
|
|
|
2998
3244
|
}
|
|
2999
3245
|
|
|
3000
3246
|
// src/processors/review-assistance/review-assistance-page-gate.ts
|
|
3001
|
-
import { existsSync as existsSync6, readFileSync as
|
|
3247
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3002
3248
|
import { join as join9 } from "path";
|
|
3003
|
-
import { z as
|
|
3249
|
+
import { z as z4 } from "zod/v4";
|
|
3004
3250
|
var REVIEW_ASSISTANCE_PAGE_GATE_FILE = "review_assistance_page_gate.json";
|
|
3005
3251
|
var REVIEW_ASSISTANCE_PAGE_GATE_MAX_RETRIES = 2;
|
|
3006
3252
|
var REVIEW_ASSISTANCE_PAGE_GATE_TEMPERATURE = 0;
|
|
3007
3253
|
var MAX_REASON_COUNT = 12;
|
|
3008
3254
|
var MAX_CONTEXT_TEXT_LENGTH = 1800;
|
|
3009
3255
|
var REVIEW_ASSISTANCE_PAGE_IMAGE_NOT_AVAILABLE_REASON = "page_image_not_available";
|
|
3010
|
-
var reviewAssistancePageGateSchema =
|
|
3011
|
-
eligible:
|
|
3012
|
-
kind:
|
|
3013
|
-
score:
|
|
3014
|
-
reasons:
|
|
3015
|
-
exclusionReasons:
|
|
3256
|
+
var reviewAssistancePageGateSchema = z4.object({
|
|
3257
|
+
eligible: z4.boolean(),
|
|
3258
|
+
kind: z4.enum(["toc", "archaeological_data", "non_meaningful"]),
|
|
3259
|
+
score: z4.number().min(0).max(100),
|
|
3260
|
+
reasons: z4.array(z4.string()).max(MAX_REASON_COUNT),
|
|
3261
|
+
exclusionReasons: z4.array(z4.string()).max(MAX_REASON_COUNT)
|
|
3016
3262
|
});
|
|
3017
3263
|
var REVIEW_ASSISTANCE_PAGE_GATE_PROMPT = `You are deciding whether a single PDF page should run structural Review Assistance after OCR text correction.
|
|
3018
3264
|
|
|
@@ -3149,7 +3395,7 @@ function readReviewAssistancePageGateReport(outputDir) {
|
|
|
3149
3395
|
const path = join9(outputDir, REVIEW_ASSISTANCE_PAGE_GATE_FILE);
|
|
3150
3396
|
if (!existsSync6(path)) return void 0;
|
|
3151
3397
|
const report = JSON.parse(
|
|
3152
|
-
|
|
3398
|
+
readFileSync3(path, "utf-8")
|
|
3153
3399
|
);
|
|
3154
3400
|
return new Map(report.pages.map((page) => [page.pageNo, page]));
|
|
3155
3401
|
}
|
|
@@ -3167,7 +3413,7 @@ function writeReviewAssistancePageGateReport(outputDir, pages) {
|
|
|
3167
3413
|
|
|
3168
3414
|
// src/processors/post-docling-page-processor.ts
|
|
3169
3415
|
var DEFAULT_CONCURRENCY = 1;
|
|
3170
|
-
var
|
|
3416
|
+
var DEFAULT_MAX_RETRIES2 = 3;
|
|
3171
3417
|
var DEFAULT_TEMPERATURE = 0;
|
|
3172
3418
|
var PostDoclingPageProcessor = class {
|
|
3173
3419
|
constructor(logger) {
|
|
@@ -3185,7 +3431,7 @@ var PostDoclingPageProcessor = class {
|
|
|
3185
3431
|
async correctAndSave(outputDir, model, options) {
|
|
3186
3432
|
this.logger.info("[PostDoclingPageProcessor] Starting text correction...");
|
|
3187
3433
|
const resultPath = join10(outputDir, "result.json");
|
|
3188
|
-
const doc = JSON.parse(
|
|
3434
|
+
const doc = JSON.parse(readFileSync4(resultPath, "utf-8"));
|
|
3189
3435
|
const allPageNumbers = this.getPageNumbers(doc);
|
|
3190
3436
|
if (allPageNumbers.length === 0) {
|
|
3191
3437
|
this.logger.info("[PostDoclingPageProcessor] No pages to process");
|
|
@@ -3327,7 +3573,7 @@ var PostDoclingPageProcessor = class {
|
|
|
3327
3573
|
}
|
|
3328
3574
|
],
|
|
3329
3575
|
primaryModel: model,
|
|
3330
|
-
maxRetries: options?.maxRetries ??
|
|
3576
|
+
maxRetries: options?.maxRetries ?? DEFAULT_MAX_RETRIES2,
|
|
3331
3577
|
temperature: options?.temperature ?? DEFAULT_TEMPERATURE,
|
|
3332
3578
|
abortSignal: options?.abortSignal,
|
|
3333
3579
|
component: "PostDoclingPageProcessor",
|
|
@@ -3493,21 +3739,21 @@ var PostDoclingPageProcessor = class {
|
|
|
3493
3739
|
*/
|
|
3494
3740
|
readPageImage(outputDir, pageNo) {
|
|
3495
3741
|
const imagePath = join10(outputDir, "pages", `page_${pageNo - 1}.png`);
|
|
3496
|
-
return new Uint8Array(
|
|
3742
|
+
return new Uint8Array(readFileSync4(imagePath));
|
|
3497
3743
|
}
|
|
3498
3744
|
};
|
|
3499
3745
|
|
|
3500
3746
|
// src/processors/review-assistance/review-assistance-runner.ts
|
|
3501
|
-
import { copyFileSync as copyFileSync2, existsSync as existsSync9, readFileSync as
|
|
3747
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
3502
3748
|
import { readFile } from "fs/promises";
|
|
3503
3749
|
import { join as join14 } from "path";
|
|
3504
3750
|
|
|
3505
3751
|
// src/types/review-assistance-schema.ts
|
|
3506
|
-
import { z as
|
|
3752
|
+
import { z as z5 } from "zod/v4";
|
|
3507
3753
|
var REVIEW_ASSISTANCE_RATIONALE_MAX_LENGTH = 2e3;
|
|
3508
3754
|
var REVIEW_ASSISTANCE_EVIDENCE_MAX_LENGTH = 2e3;
|
|
3509
3755
|
var REVIEW_ASSISTANCE_PAGE_NOTE_MAX_LENGTH = 2e3;
|
|
3510
|
-
var reviewAssistanceCommandOpSchema =
|
|
3756
|
+
var reviewAssistanceCommandOpSchema = z5.enum([
|
|
3511
3757
|
"replaceText",
|
|
3512
3758
|
"addText",
|
|
3513
3759
|
"updateTextRole",
|
|
@@ -3525,18 +3771,18 @@ var reviewAssistanceCommandOpSchema = z4.enum([
|
|
|
3525
3771
|
"linkFootnote",
|
|
3526
3772
|
"moveNode"
|
|
3527
3773
|
]);
|
|
3528
|
-
var reviewAssistanceRawCommandSchema =
|
|
3774
|
+
var reviewAssistanceRawCommandSchema = z5.object({
|
|
3529
3775
|
op: reviewAssistanceCommandOpSchema,
|
|
3530
|
-
targetRef:
|
|
3531
|
-
payload:
|
|
3532
|
-
confidence:
|
|
3533
|
-
rationale:
|
|
3534
|
-
evidence:
|
|
3776
|
+
targetRef: z5.string().nullable(),
|
|
3777
|
+
payload: z5.record(z5.string(), z5.unknown()),
|
|
3778
|
+
confidence: z5.number().min(0).max(1),
|
|
3779
|
+
rationale: z5.string().max(REVIEW_ASSISTANCE_RATIONALE_MAX_LENGTH),
|
|
3780
|
+
evidence: z5.string().max(REVIEW_ASSISTANCE_EVIDENCE_MAX_LENGTH).nullable()
|
|
3535
3781
|
});
|
|
3536
|
-
var reviewAssistancePageSchema =
|
|
3537
|
-
pageNo:
|
|
3538
|
-
commands:
|
|
3539
|
-
pageNotes:
|
|
3782
|
+
var reviewAssistancePageSchema = z5.object({
|
|
3783
|
+
pageNo: z5.number().int().positive(),
|
|
3784
|
+
commands: z5.array(reviewAssistanceRawCommandSchema).max(80),
|
|
3785
|
+
pageNotes: z5.array(z5.string().max(REVIEW_ASSISTANCE_PAGE_NOTE_MAX_LENGTH)).max(10)
|
|
3540
3786
|
});
|
|
3541
3787
|
|
|
3542
3788
|
// src/prompts/review-assistance-prompt.ts
|
|
@@ -5029,7 +5275,7 @@ var PictureSplitCandidateDetector = class {
|
|
|
5029
5275
|
};
|
|
5030
5276
|
|
|
5031
5277
|
// src/processors/review-assistance/review-assistance-checkpoint-store.ts
|
|
5032
|
-
import { existsSync as existsSync7, readFileSync as
|
|
5278
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
5033
5279
|
import { join as join12 } from "path";
|
|
5034
5280
|
var CHECKPOINT_FILE = "review_assistance_checkpoint.json";
|
|
5035
5281
|
var ReviewAssistanceCheckpointStore = class _ReviewAssistanceCheckpointStore {
|
|
@@ -5111,7 +5357,7 @@ var ReviewAssistanceCheckpointStore = class _ReviewAssistanceCheckpointStore {
|
|
|
5111
5357
|
}
|
|
5112
5358
|
try {
|
|
5113
5359
|
const parsed = JSON.parse(
|
|
5114
|
-
|
|
5360
|
+
readFileSync5(path, "utf-8")
|
|
5115
5361
|
);
|
|
5116
5362
|
if (parsed.schemaVersion !== 1 || parsed.runId !== runId) {
|
|
5117
5363
|
return this.empty(runId, now);
|
|
@@ -7900,7 +8146,7 @@ var ReviewAssistanceRunner = class {
|
|
|
7900
8146
|
copyFileSync2(reviewOriginPath, ocrOriginPath);
|
|
7901
8147
|
}
|
|
7902
8148
|
const doc = JSON.parse(
|
|
7903
|
-
|
|
8149
|
+
readFileSync6(reviewOriginPath, "utf-8")
|
|
7904
8150
|
);
|
|
7905
8151
|
const pageTexts = options.pageTexts ?? await PdfTextExtractor.tryExtract(
|
|
7906
8152
|
this.logger,
|
|
@@ -9423,8 +9669,13 @@ var PDFConverter = class {
|
|
|
9423
9669
|
async convert(url, reportId, onComplete, cleanupAfterCallback, options, abortSignal) {
|
|
9424
9670
|
this.logger.info("[PDFConverter] Converting:", url);
|
|
9425
9671
|
const aggregator = options.aggregator ?? new LLMTokenUsageAggregator();
|
|
9426
|
-
const trackedOptions = { ...options, aggregator };
|
|
9427
9672
|
const pdfPath = url.startsWith("file://") ? url.slice(7) : void 0;
|
|
9673
|
+
const trackedOptions = await this.withDetectedLanguages(
|
|
9674
|
+
pdfPath,
|
|
9675
|
+
reportId,
|
|
9676
|
+
{ ...options, aggregator },
|
|
9677
|
+
abortSignal
|
|
9678
|
+
);
|
|
9428
9679
|
const correctedOnComplete = new PostDoclingCorrectionPipeline(
|
|
9429
9680
|
this.logger
|
|
9430
9681
|
).wrapCallback(pdfPath, reportId, trackedOptions, onComplete, abortSignal);
|
|
@@ -9487,6 +9738,58 @@ var PDFConverter = class {
|
|
|
9487
9738
|
}
|
|
9488
9739
|
return this.buildTokenReport(aggregator) ?? conversionReport;
|
|
9489
9740
|
}
|
|
9741
|
+
async withDetectedLanguages(pdfPath, reportId, options, abortSignal) {
|
|
9742
|
+
if (options.ocr_lang && options.ocr_lang.length > 0) {
|
|
9743
|
+
this.logger.info(
|
|
9744
|
+
`[PDFConverter] OCR languages provided: ${JSON.stringify(options.ocr_lang)}`
|
|
9745
|
+
);
|
|
9746
|
+
return options;
|
|
9747
|
+
}
|
|
9748
|
+
if (!pdfPath) {
|
|
9749
|
+
this.logger.info(
|
|
9750
|
+
"[PDFConverter] Local PDF not available; OCR languages will use defaults"
|
|
9751
|
+
);
|
|
9752
|
+
return {
|
|
9753
|
+
...options,
|
|
9754
|
+
ocr_lang: [...DEFAULT_OCR_LANGUAGES]
|
|
9755
|
+
};
|
|
9756
|
+
}
|
|
9757
|
+
const languageDetectionDir = join16(
|
|
9758
|
+
process.cwd(),
|
|
9759
|
+
"output",
|
|
9760
|
+
reportId,
|
|
9761
|
+
"_language_detection"
|
|
9762
|
+
);
|
|
9763
|
+
const detector = new PdfLanguageDetector(
|
|
9764
|
+
this.logger,
|
|
9765
|
+
new PageRenderer(this.logger),
|
|
9766
|
+
new PdfTextExtractor(this.logger)
|
|
9767
|
+
);
|
|
9768
|
+
try {
|
|
9769
|
+
const result = await detector.detect(pdfPath, languageDetectionDir, {
|
|
9770
|
+
model: options.languageDetectionModel,
|
|
9771
|
+
aggregator: options.aggregator,
|
|
9772
|
+
abortSignal
|
|
9773
|
+
});
|
|
9774
|
+
this.logger.info(
|
|
9775
|
+
`[PDFConverter] OCR languages detected: ${JSON.stringify(result.detectedLanguages)} (${result.reason})`
|
|
9776
|
+
);
|
|
9777
|
+
if (options.onTokenUsage && options.aggregator) {
|
|
9778
|
+
const tokenReport = this.buildTokenReport(options.aggregator);
|
|
9779
|
+
if (tokenReport) {
|
|
9780
|
+
options.onTokenUsage(tokenReport);
|
|
9781
|
+
}
|
|
9782
|
+
}
|
|
9783
|
+
return {
|
|
9784
|
+
...options,
|
|
9785
|
+
ocr_lang: result.detectedLanguages
|
|
9786
|
+
};
|
|
9787
|
+
} finally {
|
|
9788
|
+
if (existsSync10(languageDetectionDir)) {
|
|
9789
|
+
rmSync7(languageDetectionDir, { recursive: true, force: true });
|
|
9790
|
+
}
|
|
9791
|
+
}
|
|
9792
|
+
}
|
|
9490
9793
|
/**
|
|
9491
9794
|
* Build a token usage report from the aggregator.
|
|
9492
9795
|
* Returns null when no LLM calls were tracked.
|
|
@@ -9645,7 +9948,7 @@ var PDFParser = class {
|
|
|
9645
9948
|
this.baseUrl = void 0;
|
|
9646
9949
|
}
|
|
9647
9950
|
this.timeout = timeout;
|
|
9648
|
-
this.venvPath = venvPath ||
|
|
9951
|
+
this.venvPath = venvPath || join17(process.cwd(), ".venv");
|
|
9649
9952
|
this.killExistingProcess = killExistingProcess;
|
|
9650
9953
|
this.enableImagePdfFallback = enableImagePdfFallback;
|
|
9651
9954
|
}
|
|
@@ -9942,7 +10245,7 @@ var META_DESCRIPTION_PATTERNS_EN = [
|
|
|
9942
10245
|
];
|
|
9943
10246
|
var REPETITIVE_PATTERN_RATIO_THRESHOLD = 0.3;
|
|
9944
10247
|
var REPETITIVE_PATTERN_MIN_REPEATS = 5;
|
|
9945
|
-
var
|
|
10248
|
+
var HANGUL_REGEX2 = /[\uAC00-\uD7AF\u1100-\u11FF]/g;
|
|
9946
10249
|
var CJK_REGEX = /[\u4E00-\u9FFF]/g;
|
|
9947
10250
|
var VlmResponseValidator = class {
|
|
9948
10251
|
/**
|
|
@@ -10010,7 +10313,7 @@ var VlmResponseValidator = class {
|
|
|
10010
10313
|
if (nonWhitespace.length < MIN_CONTENT_LENGTH) {
|
|
10011
10314
|
return null;
|
|
10012
10315
|
}
|
|
10013
|
-
const hangulCount = allContent.match(
|
|
10316
|
+
const hangulCount = allContent.match(HANGUL_REGEX2)?.length ?? 0;
|
|
10014
10317
|
const cjkCount = allContent.match(CJK_REGEX)?.length ?? 0;
|
|
10015
10318
|
const koreanCjkCount = hangulCount + cjkCount;
|
|
10016
10319
|
const ratio = koreanCjkCount / nonWhitespace.length;
|