@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.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/core/pdf-parser.ts
2
2
  import { Docling } from "docling-sdk";
3
- import { join as join16 } from "path";
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 OCR strategy sampling */
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/errors/image-pdf-fallback-error.ts
985
- var ImagePdfFallbackError = class extends Error {
986
- constructor(originalError, fallbackError) {
987
- super(
988
- `PDF conversion failed with fallback. Original: ${originalError.message}. Fallback: ${fallbackError.message}`
989
- );
990
- this.originalError = originalError;
991
- this.fallbackError = fallbackError;
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 = z.object({
1166
- isValid: z.boolean().describe("Whether this is an archaeological investigation report"),
1167
- reason: z.string().describe("Brief reason for the decision")
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 join3 } from "path";
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 = join3(targetDir, entry.fileName);
1943
+ const entryPath = join4(targetDir, entry.fileName);
1579
1944
  if (/\/$/.test(entry.fileName)) {
1580
- mkdirSync(entryPath, { recursive: true });
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
- mkdirSync(join3(entryPath, ".."), { recursive: true });
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 = join3(imagesDir, filename);
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 = join3(imagesDir, filename);
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 (existsSync(outputDir)) {
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
- mkdirSync(outputDir, { recursive: true });
2061
+ mkdirSync2(outputDir, { recursive: true });
1697
2062
  const baseName = filename.replace(extname(filename), "");
1698
- const jsonPath = join3(outputDir, `${baseName}.json`);
2063
+ const jsonPath = join4(outputDir, `${baseName}.json`);
1699
2064
  try {
1700
- const imagesDir = join3(outputDir, "images");
1701
- if (!existsSync(imagesDir)) {
1702
- mkdirSync(imagesDir, { recursive: true });
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 = join3(outputDir, `${baseName}.html`);
2101
+ const htmlPath = join4(outputDir, `${baseName}.html`);
1737
2102
  try {
1738
- const imagesDir = join3(outputDir, "images");
1739
- if (!existsSync(imagesDir)) {
1740
- mkdirSync(imagesDir, { recursive: true });
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 = readdirSync(extractDir);
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 = join3(extractDir, jsonFile);
1786
- const htmlPath = join3(extractDir, htmlFile);
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: options.ocr_lang ?? ["ko-KR", "en-US"],
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 = readFileSync(resultPath, "utf-8");
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 readFileSync3, writeFileSync as writeFileSync4 } from "fs";
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 z2 } from "zod/v4";
2836
- var vlmTextCorrectionSchema = z2.object({
3081
+ import { z as z3 } from "zod/v4";
3082
+ var vlmTextCorrectionSchema = z3.object({
2837
3083
  /** Text element corrections (substitution-based) */
2838
- tc: z2.array(
2839
- z2.object({
3084
+ tc: z3.array(
3085
+ z3.object({
2840
3086
  /** Text element index (from prompt) */
2841
- i: z2.number().int().nonnegative(),
3087
+ i: z3.number().int().nonnegative(),
2842
3088
  /** Substitutions: find/replace pairs applied left-to-right */
2843
- s: z2.array(
2844
- z2.object({
3089
+ s: z3.array(
3090
+ z3.object({
2845
3091
  /** Exact garbled substring to find */
2846
- f: z2.string(),
3092
+ f: z3.string(),
2847
3093
  /** Corrected replacement text */
2848
- r: z2.string()
3094
+ r: z3.string()
2849
3095
  })
2850
3096
  )
2851
3097
  })
2852
3098
  ),
2853
3099
  /** Table cell corrections */
2854
- cc: z2.array(
2855
- z2.object({
3100
+ cc: z3.array(
3101
+ z3.object({
2856
3102
  /** Table index (within the page) */
2857
- ti: z2.number().int().nonnegative(),
3103
+ ti: z3.number().int().nonnegative(),
2858
3104
  /** Row index */
2859
- r: z2.number().int().nonnegative(),
3105
+ r: z3.number().int().nonnegative(),
2860
3106
  /** Column index */
2861
- c: z2.number().int().nonnegative(),
3107
+ c: z3.number().int().nonnegative(),
2862
3108
  /** Corrected cell text */
2863
- t: z2.string()
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 readFileSync2, writeFileSync as writeFileSync3 } from "fs";
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 z3 } from "zod/v4";
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 = z3.object({
3011
- eligible: z3.boolean(),
3012
- kind: z3.enum(["toc", "archaeological_data", "non_meaningful"]),
3013
- score: z3.number().min(0).max(100),
3014
- reasons: z3.array(z3.string()).max(MAX_REASON_COUNT),
3015
- exclusionReasons: z3.array(z3.string()).max(MAX_REASON_COUNT)
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
- readFileSync2(path, "utf-8")
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 DEFAULT_MAX_RETRIES = 3;
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(readFileSync3(resultPath, "utf-8"));
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 ?? DEFAULT_MAX_RETRIES,
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(readFileSync3(imagePath));
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 readFileSync5, writeFileSync as writeFileSync6 } from "fs";
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 z4 } from "zod/v4";
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 = z4.enum([
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 = z4.object({
3774
+ var reviewAssistanceRawCommandSchema = z5.object({
3529
3775
  op: reviewAssistanceCommandOpSchema,
3530
- targetRef: z4.string().nullable(),
3531
- payload: z4.record(z4.string(), z4.unknown()),
3532
- confidence: z4.number().min(0).max(1),
3533
- rationale: z4.string().max(REVIEW_ASSISTANCE_RATIONALE_MAX_LENGTH),
3534
- evidence: z4.string().max(REVIEW_ASSISTANCE_EVIDENCE_MAX_LENGTH).nullable()
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 = z4.object({
3537
- pageNo: z4.number().int().positive(),
3538
- commands: z4.array(reviewAssistanceRawCommandSchema).max(80),
3539
- pageNotes: z4.array(z4.string().max(REVIEW_ASSISTANCE_PAGE_NOTE_MAX_LENGTH)).max(10)
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 readFileSync4, writeFileSync as writeFileSync5 } from "fs";
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
- readFileSync4(path, "utf-8")
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
- readFileSync5(reviewOriginPath, "utf-8")
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 || join16(process.cwd(), ".venv");
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 HANGUL_REGEX = /[\uAC00-\uD7AF\u1100-\u11FF]/g;
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(HANGUL_REGEX)?.length ?? 0;
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;