@heripo/pdf-parser 0.1.27 → 0.1.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +95 -18
- package/README.md +96 -18
- package/dist/index.cjs +41 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +41 -77
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -6525,15 +6525,12 @@ import { normalizeToBcp47 } from "@heripo/model";
|
|
|
6525
6525
|
import { readFileSync as readFileSync3 } from "fs";
|
|
6526
6526
|
import { z as z3 } from "zod/v4";
|
|
6527
6527
|
|
|
6528
|
-
// src/prompts/korean-
|
|
6529
|
-
var
|
|
6528
|
+
// src/prompts/korean-document-detection-prompt.ts
|
|
6529
|
+
var KOREAN_DOCUMENT_DETECTION_PROMPT = `Look at this page image carefully and identify all languages present on this page.
|
|
6530
6530
|
|
|
6531
|
-
|
|
6532
|
-
|
|
6531
|
+
Return an array of ocrmac-compatible language tags ordered by prevalence (primary language first).
|
|
6532
|
+
Use ko-KR when Korean text is present, even when the page also contains Hanja (\u6F22\u5B57/Chinese characters), English, numbers, tables, or captions.
|
|
6533
6533
|
|
|
6534
|
-
Answer whether any Hanja characters are present on this page.
|
|
6535
|
-
|
|
6536
|
-
Also identify all languages present on this page. Return an array of ocrmac-compatible language tags ordered by prevalence (primary language first).
|
|
6537
6534
|
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.
|
|
6538
6535
|
Examples: ["ko-KR", "en-US"], ["ja-JP"], ["zh-Hant", "en-US"]`;
|
|
6539
6536
|
|
|
@@ -6541,12 +6538,8 @@ Examples: ["ko-KR", "en-US"], ["ja-JP"], ["zh-Hant", "en-US"]`;
|
|
|
6541
6538
|
var EDGE_TRIM_RATIO = 0.1;
|
|
6542
6539
|
var DEFAULT_MAX_SAMPLE_PAGES = 15;
|
|
6543
6540
|
var DEFAULT_MAX_RETRIES = 3;
|
|
6544
|
-
var CJK_REGEX = /[\u4E00-\u9FFF]/;
|
|
6545
6541
|
var HANGUL_REGEX = /[\uAC00-\uD7AF]/;
|
|
6546
|
-
var
|
|
6547
|
-
hasKoreanHanjaMix: z3.boolean().describe(
|
|
6548
|
-
"Whether the page contains any Hanja (\u6F22\u5B57/Chinese characters) mixed with Korean text"
|
|
6549
|
-
),
|
|
6542
|
+
var koreanDocumentDetectionSchema = z3.object({
|
|
6550
6543
|
detectedLanguages: z3.array(z3.string()).describe(
|
|
6551
6544
|
'BCP 47 language tags of languages found on this page, ordered by prevalence (e.g., ["ko-KR", "en-US"])'
|
|
6552
6545
|
)
|
|
@@ -6565,14 +6558,14 @@ var OcrStrategySampler = class {
|
|
|
6565
6558
|
*
|
|
6566
6559
|
* @param pdfPath - Path to the PDF file
|
|
6567
6560
|
* @param outputDir - Directory for temporary rendered pages
|
|
6568
|
-
* @param model - Vision language model for Korean
|
|
6561
|
+
* @param model - Vision language model for Korean document detection
|
|
6569
6562
|
* @param options - Sampling options
|
|
6570
6563
|
* @returns OcrStrategy with method ('ocrmac' or 'vlm') and metadata
|
|
6571
6564
|
*/
|
|
6572
6565
|
async sample(pdfPath, outputDir, model, options) {
|
|
6573
6566
|
const maxSamplePages = options?.maxSamplePages ?? DEFAULT_MAX_SAMPLE_PAGES;
|
|
6574
6567
|
this.logger.info("[OcrStrategySampler] Starting OCR strategy sampling...");
|
|
6575
|
-
const preCheckResult = await this.
|
|
6568
|
+
const preCheckResult = await this.preCheckKoreanFromTextLayer(pdfPath);
|
|
6576
6569
|
if (preCheckResult) {
|
|
6577
6570
|
return preCheckResult;
|
|
6578
6571
|
}
|
|
@@ -6603,9 +6596,9 @@ var OcrStrategySampler = class {
|
|
|
6603
6596
|
model,
|
|
6604
6597
|
options
|
|
6605
6598
|
);
|
|
6606
|
-
if (sampleResult.
|
|
6599
|
+
if (sampleResult.foundKorean) {
|
|
6607
6600
|
this.logger.info(
|
|
6608
|
-
`[OcrStrategySampler] Korean
|
|
6601
|
+
`[OcrStrategySampler] Korean document detected on page ${sampleResult.koreanPageNo} \u2192 VLM strategy`
|
|
6609
6602
|
);
|
|
6610
6603
|
const detectedLanguages2 = this.aggregateLanguages(
|
|
6611
6604
|
sampleResult.languageFrequency
|
|
@@ -6613,13 +6606,13 @@ var OcrStrategySampler = class {
|
|
|
6613
6606
|
return {
|
|
6614
6607
|
method: "vlm",
|
|
6615
6608
|
detectedLanguages: detectedLanguages2,
|
|
6616
|
-
reason: `Korean
|
|
6609
|
+
reason: `Korean document detected on page ${sampleResult.koreanPageNo}`,
|
|
6617
6610
|
sampledPages: sampleResult.sampledCount,
|
|
6618
6611
|
totalPages: renderResult.pageCount
|
|
6619
6612
|
};
|
|
6620
6613
|
}
|
|
6621
6614
|
this.logger.info(
|
|
6622
|
-
"[OcrStrategySampler] No Korean
|
|
6615
|
+
"[OcrStrategySampler] No Korean language detected \u2192 ocrmac strategy"
|
|
6623
6616
|
);
|
|
6624
6617
|
const detectedLanguages = this.aggregateLanguages(
|
|
6625
6618
|
sampleResult.languageFrequency
|
|
@@ -6627,13 +6620,13 @@ var OcrStrategySampler = class {
|
|
|
6627
6620
|
return {
|
|
6628
6621
|
method: "ocrmac",
|
|
6629
6622
|
detectedLanguages,
|
|
6630
|
-
reason: `No Korean
|
|
6623
|
+
reason: `No Korean language detected in ${sampleResult.sampledCount} sampled pages`,
|
|
6631
6624
|
sampledPages: sampleResult.sampledCount,
|
|
6632
6625
|
totalPages: renderResult.pageCount
|
|
6633
6626
|
};
|
|
6634
6627
|
}
|
|
6635
6628
|
/**
|
|
6636
|
-
* Sample pages for Korean
|
|
6629
|
+
* Sample pages for Korean language detection with early exit.
|
|
6637
6630
|
*/
|
|
6638
6631
|
async samplePages(sampleIndices, pageFiles, model, options) {
|
|
6639
6632
|
const languageFrequency = /* @__PURE__ */ new Map();
|
|
@@ -6649,72 +6642,52 @@ var OcrStrategySampler = class {
|
|
|
6649
6642
|
for (const lang of pageAnalysis.detectedLanguages) {
|
|
6650
6643
|
languageFrequency.set(lang, (languageFrequency.get(lang) ?? 0) + 1);
|
|
6651
6644
|
}
|
|
6652
|
-
if (pageAnalysis.
|
|
6645
|
+
if (pageAnalysis.detectedLanguages.includes("ko-KR")) {
|
|
6653
6646
|
return {
|
|
6654
|
-
|
|
6655
|
-
|
|
6647
|
+
foundKorean: true,
|
|
6648
|
+
koreanPageNo: idx + 1,
|
|
6656
6649
|
sampledCount: i + 1,
|
|
6657
6650
|
languageFrequency
|
|
6658
6651
|
};
|
|
6659
6652
|
}
|
|
6660
6653
|
}
|
|
6661
6654
|
return {
|
|
6662
|
-
|
|
6655
|
+
foundKorean: false,
|
|
6663
6656
|
sampledCount: sampleIndices.length,
|
|
6664
6657
|
languageFrequency
|
|
6665
6658
|
};
|
|
6666
6659
|
}
|
|
6667
6660
|
/**
|
|
6668
|
-
* Pre-check for
|
|
6669
|
-
* Extracts full document text in a single process and checks at document level
|
|
6670
|
-
*
|
|
6671
|
-
* - Hangul + Hanja (anywhere in document) → VLM (confirmed Korean-Hanja mix)
|
|
6672
|
-
* - Hangul only → ocrmac with ko-KR (confirmed Korean)
|
|
6661
|
+
* Pre-check for Korean text in PDF text layer using pdftotext.
|
|
6662
|
+
* Extracts full document text in a single process and checks at document level:
|
|
6663
|
+
* - Hangul anywhere in document → VLM (confirmed Korean document)
|
|
6673
6664
|
* - No Hangul (English, Japanese, etc.) → null (delegates to VLM for language detection)
|
|
6674
6665
|
*/
|
|
6675
|
-
async
|
|
6666
|
+
async preCheckKoreanFromTextLayer(pdfPath) {
|
|
6676
6667
|
try {
|
|
6677
6668
|
const totalPages = await this.textExtractor.getPageCount(pdfPath);
|
|
6678
6669
|
if (totalPages === 0) return null;
|
|
6679
6670
|
const fullText = await this.textExtractor.extractFullText(pdfPath);
|
|
6680
6671
|
if (fullText.trim().length === 0) {
|
|
6681
6672
|
this.logger.debug(
|
|
6682
|
-
"[OcrStrategySampler] No
|
|
6673
|
+
"[OcrStrategySampler] No text in text layer, falling back to VLM sampling"
|
|
6683
6674
|
);
|
|
6684
6675
|
return null;
|
|
6685
6676
|
}
|
|
6686
6677
|
const hasHangul = HANGUL_REGEX.test(fullText);
|
|
6687
|
-
const hasHanja = CJK_REGEX.test(fullText);
|
|
6688
6678
|
if (!hasHangul) {
|
|
6689
6679
|
this.logger.debug(
|
|
6690
6680
|
"[OcrStrategySampler] No Hangul in text layer, falling back to VLM sampling"
|
|
6691
6681
|
);
|
|
6692
6682
|
return null;
|
|
6693
6683
|
}
|
|
6694
|
-
if (hasHanja) {
|
|
6695
|
-
const pageTextArray = fullText.split("\f");
|
|
6696
|
-
const koreanHanjaMixPages = pageTextArray.flatMap(
|
|
6697
|
-
(text, i) => CJK_REGEX.test(text) ? [i + 1] : []
|
|
6698
|
-
);
|
|
6699
|
-
this.logger.info(
|
|
6700
|
-
`[OcrStrategySampler] Hangul-Hanja mix detected in text layer \u2192 VLM strategy (${koreanHanjaMixPages.length} pages with Hanja)`
|
|
6701
|
-
);
|
|
6702
|
-
return {
|
|
6703
|
-
method: "vlm",
|
|
6704
|
-
detectedLanguages: ["ko-KR"],
|
|
6705
|
-
reason: "Hangul-Hanja mix found in PDF text layer",
|
|
6706
|
-
koreanHanjaMixPages,
|
|
6707
|
-
sampledPages: totalPages,
|
|
6708
|
-
totalPages
|
|
6709
|
-
};
|
|
6710
|
-
}
|
|
6711
6684
|
this.logger.info(
|
|
6712
|
-
"[OcrStrategySampler]
|
|
6685
|
+
"[OcrStrategySampler] Korean text detected in text layer \u2192 VLM strategy"
|
|
6713
6686
|
);
|
|
6714
6687
|
return {
|
|
6715
|
-
method: "
|
|
6688
|
+
method: "vlm",
|
|
6716
6689
|
detectedLanguages: ["ko-KR"],
|
|
6717
|
-
reason: `
|
|
6690
|
+
reason: `Korean text found in PDF text layer (${totalPages} pages checked)`,
|
|
6718
6691
|
sampledPages: totalPages,
|
|
6719
6692
|
totalPages
|
|
6720
6693
|
};
|
|
@@ -6755,21 +6728,24 @@ var OcrStrategySampler = class {
|
|
|
6755
6728
|
);
|
|
6756
6729
|
}
|
|
6757
6730
|
/**
|
|
6758
|
-
* Analyze a single sample page for
|
|
6731
|
+
* Analyze a single sample page for language.
|
|
6759
6732
|
* Normalizes raw VLM language responses to valid BCP 47 tags, filtering out invalid ones.
|
|
6760
6733
|
*
|
|
6761
|
-
* @returns Object with
|
|
6734
|
+
* @returns Object with normalized detected languages
|
|
6762
6735
|
*/
|
|
6763
6736
|
async analyzeSamplePage(pageFile, pageNo, model, options) {
|
|
6764
6737
|
this.logger.debug(
|
|
6765
|
-
`[OcrStrategySampler] Analyzing page ${pageNo} for
|
|
6738
|
+
`[OcrStrategySampler] Analyzing page ${pageNo} for language...`
|
|
6766
6739
|
);
|
|
6767
6740
|
const imageData = new Uint8Array(readFileSync3(pageFile));
|
|
6768
6741
|
const messages = [
|
|
6769
6742
|
{
|
|
6770
6743
|
role: "user",
|
|
6771
6744
|
content: [
|
|
6772
|
-
{
|
|
6745
|
+
{
|
|
6746
|
+
type: "text",
|
|
6747
|
+
text: KOREAN_DOCUMENT_DETECTION_PROMPT
|
|
6748
|
+
},
|
|
6773
6749
|
{
|
|
6774
6750
|
type: "image",
|
|
6775
6751
|
image: imageData,
|
|
@@ -6779,7 +6755,7 @@ var OcrStrategySampler = class {
|
|
|
6779
6755
|
}
|
|
6780
6756
|
];
|
|
6781
6757
|
const result = await LLMCaller.callVision({
|
|
6782
|
-
schema:
|
|
6758
|
+
schema: koreanDocumentDetectionSchema,
|
|
6783
6759
|
messages,
|
|
6784
6760
|
primaryModel: model,
|
|
6785
6761
|
fallbackModel: options?.fallbackModel,
|
|
@@ -6787,7 +6763,7 @@ var OcrStrategySampler = class {
|
|
|
6787
6763
|
temperature: options?.temperature ?? 0,
|
|
6788
6764
|
abortSignal: options?.abortSignal,
|
|
6789
6765
|
component: "OcrStrategySampler",
|
|
6790
|
-
phase: "korean-
|
|
6766
|
+
phase: "korean-document-detection"
|
|
6791
6767
|
});
|
|
6792
6768
|
if (options?.aggregator) {
|
|
6793
6769
|
options.aggregator.track(result.usage);
|
|
@@ -6795,10 +6771,9 @@ var OcrStrategySampler = class {
|
|
|
6795
6771
|
const output = result.output;
|
|
6796
6772
|
const normalizedLanguages = output.detectedLanguages.map(normalizeToBcp47).filter((tag) => tag !== null);
|
|
6797
6773
|
this.logger.debug(
|
|
6798
|
-
`[OcrStrategySampler] Page ${pageNo}:
|
|
6774
|
+
`[OcrStrategySampler] Page ${pageNo}: detectedLanguages=${normalizedLanguages.join(",")}`
|
|
6799
6775
|
);
|
|
6800
6776
|
return {
|
|
6801
|
-
hasKoreanHanjaMix: output.hasKoreanHanjaMix,
|
|
6802
6777
|
detectedLanguages: normalizedLanguages
|
|
6803
6778
|
};
|
|
6804
6779
|
}
|
|
@@ -7047,16 +7022,7 @@ var VlmTextCorrector = class {
|
|
|
7047
7022
|
pagesFailed: 0
|
|
7048
7023
|
};
|
|
7049
7024
|
}
|
|
7050
|
-
|
|
7051
|
-
if (options?.koreanHanjaMixPages && options.koreanHanjaMixPages.length > 0) {
|
|
7052
|
-
const hanjaSet = new Set(options.koreanHanjaMixPages);
|
|
7053
|
-
pageNumbers = allPageNumbers.filter((p) => hanjaSet.has(p));
|
|
7054
|
-
this.logger.info(
|
|
7055
|
-
`[VlmTextCorrector] Filtering to ${pageNumbers.length} Korean-Hanja mix pages out of ${allPageNumbers.length} total`
|
|
7056
|
-
);
|
|
7057
|
-
} else {
|
|
7058
|
-
pageNumbers = allPageNumbers;
|
|
7059
|
-
}
|
|
7025
|
+
const pageNumbers = allPageNumbers;
|
|
7060
7026
|
const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY;
|
|
7061
7027
|
this.logger.info(
|
|
7062
7028
|
`[VlmTextCorrector] Processing ${pageNumbers.length} pages (concurrency: ${concurrency})...`
|
|
@@ -7261,7 +7227,7 @@ var VlmConversionPipeline = class {
|
|
|
7261
7227
|
* Wrap the original callback with VLM text correction.
|
|
7262
7228
|
* Returns a new callback that runs VLM correction before calling the original.
|
|
7263
7229
|
*/
|
|
7264
|
-
wrapCallback(pdfPath, options, originalCallback, abortSignal, detectedLanguages
|
|
7230
|
+
wrapCallback(pdfPath, options, originalCallback, abortSignal, detectedLanguages) {
|
|
7265
7231
|
if (!options.vlmProcessorModel) {
|
|
7266
7232
|
throw new Error("vlmProcessorModel is required when OCR strategy is VLM");
|
|
7267
7233
|
}
|
|
@@ -7293,8 +7259,7 @@ var VlmConversionPipeline = class {
|
|
|
7293
7259
|
abortSignal,
|
|
7294
7260
|
onTokenUsage: options.onTokenUsage,
|
|
7295
7261
|
documentLanguages: detectedLanguages,
|
|
7296
|
-
pageTexts
|
|
7297
|
-
koreanHanjaMixPages
|
|
7262
|
+
pageTexts
|
|
7298
7263
|
}
|
|
7299
7264
|
);
|
|
7300
7265
|
await originalCallback(outputDir);
|
|
@@ -7446,8 +7411,7 @@ var PDFConverter = class {
|
|
|
7446
7411
|
trackedOptions,
|
|
7447
7412
|
postCorrectionCallback,
|
|
7448
7413
|
abortSignal,
|
|
7449
|
-
strategy.detectedLanguages
|
|
7450
|
-
strategy.koreanHanjaMixPages
|
|
7414
|
+
strategy.detectedLanguages
|
|
7451
7415
|
);
|
|
7452
7416
|
const vlmOptions = strategy.detectedLanguages ? { ...trackedOptions, ocr_lang: strategy.detectedLanguages } : trackedOptions;
|
|
7453
7417
|
await this.convert(
|
|
@@ -7982,7 +7946,7 @@ var META_DESCRIPTION_PATTERNS_EN = [
|
|
|
7982
7946
|
var REPETITIVE_PATTERN_RATIO_THRESHOLD = 0.3;
|
|
7983
7947
|
var REPETITIVE_PATTERN_MIN_REPEATS = 5;
|
|
7984
7948
|
var HANGUL_REGEX2 = /[\uAC00-\uD7AF\u1100-\u11FF]/g;
|
|
7985
|
-
var
|
|
7949
|
+
var CJK_REGEX = /[\u4E00-\u9FFF]/g;
|
|
7986
7950
|
var VlmResponseValidator = class {
|
|
7987
7951
|
/**
|
|
7988
7952
|
* Validate VLM page result quality.
|
|
@@ -8050,7 +8014,7 @@ var VlmResponseValidator = class {
|
|
|
8050
8014
|
return null;
|
|
8051
8015
|
}
|
|
8052
8016
|
const hangulCount = allContent.match(HANGUL_REGEX2)?.length ?? 0;
|
|
8053
|
-
const cjkCount = allContent.match(
|
|
8017
|
+
const cjkCount = allContent.match(CJK_REGEX)?.length ?? 0;
|
|
8054
8018
|
const koreanCjkCount = hangulCount + cjkCount;
|
|
8055
8019
|
const ratio = koreanCjkCount / nonWhitespace.length;
|
|
8056
8020
|
if (ratio < KOREAN_SCRIPT_RATIO_THRESHOLD) {
|