@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.cjs
CHANGED
|
@@ -6545,15 +6545,12 @@ var import_model = require("@heripo/model");
|
|
|
6545
6545
|
var import_node_fs12 = require("fs");
|
|
6546
6546
|
var import_v42 = require("zod/v4");
|
|
6547
6547
|
|
|
6548
|
-
// src/prompts/korean-
|
|
6549
|
-
var
|
|
6548
|
+
// src/prompts/korean-document-detection-prompt.ts
|
|
6549
|
+
var KOREAN_DOCUMENT_DETECTION_PROMPT = `Look at this page image carefully and identify all languages present on this page.
|
|
6550
6550
|
|
|
6551
|
-
|
|
6552
|
-
|
|
6551
|
+
Return an array of ocrmac-compatible language tags ordered by prevalence (primary language first).
|
|
6552
|
+
Use ko-KR when Korean text is present, even when the page also contains Hanja (\u6F22\u5B57/Chinese characters), English, numbers, tables, or captions.
|
|
6553
6553
|
|
|
6554
|
-
Answer whether any Hanja characters are present on this page.
|
|
6555
|
-
|
|
6556
|
-
Also identify all languages present on this page. Return an array of ocrmac-compatible language tags ordered by prevalence (primary language first).
|
|
6557
6554
|
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.
|
|
6558
6555
|
Examples: ["ko-KR", "en-US"], ["ja-JP"], ["zh-Hant", "en-US"]`;
|
|
6559
6556
|
|
|
@@ -6561,12 +6558,8 @@ Examples: ["ko-KR", "en-US"], ["ja-JP"], ["zh-Hant", "en-US"]`;
|
|
|
6561
6558
|
var EDGE_TRIM_RATIO = 0.1;
|
|
6562
6559
|
var DEFAULT_MAX_SAMPLE_PAGES = 15;
|
|
6563
6560
|
var DEFAULT_MAX_RETRIES = 3;
|
|
6564
|
-
var CJK_REGEX = /[\u4E00-\u9FFF]/;
|
|
6565
6561
|
var HANGUL_REGEX = /[\uAC00-\uD7AF]/;
|
|
6566
|
-
var
|
|
6567
|
-
hasKoreanHanjaMix: import_v42.z.boolean().describe(
|
|
6568
|
-
"Whether the page contains any Hanja (\u6F22\u5B57/Chinese characters) mixed with Korean text"
|
|
6569
|
-
),
|
|
6562
|
+
var koreanDocumentDetectionSchema = import_v42.z.object({
|
|
6570
6563
|
detectedLanguages: import_v42.z.array(import_v42.z.string()).describe(
|
|
6571
6564
|
'BCP 47 language tags of languages found on this page, ordered by prevalence (e.g., ["ko-KR", "en-US"])'
|
|
6572
6565
|
)
|
|
@@ -6585,14 +6578,14 @@ var OcrStrategySampler = class {
|
|
|
6585
6578
|
*
|
|
6586
6579
|
* @param pdfPath - Path to the PDF file
|
|
6587
6580
|
* @param outputDir - Directory for temporary rendered pages
|
|
6588
|
-
* @param model - Vision language model for Korean
|
|
6581
|
+
* @param model - Vision language model for Korean document detection
|
|
6589
6582
|
* @param options - Sampling options
|
|
6590
6583
|
* @returns OcrStrategy with method ('ocrmac' or 'vlm') and metadata
|
|
6591
6584
|
*/
|
|
6592
6585
|
async sample(pdfPath, outputDir, model, options) {
|
|
6593
6586
|
const maxSamplePages = options?.maxSamplePages ?? DEFAULT_MAX_SAMPLE_PAGES;
|
|
6594
6587
|
this.logger.info("[OcrStrategySampler] Starting OCR strategy sampling...");
|
|
6595
|
-
const preCheckResult = await this.
|
|
6588
|
+
const preCheckResult = await this.preCheckKoreanFromTextLayer(pdfPath);
|
|
6596
6589
|
if (preCheckResult) {
|
|
6597
6590
|
return preCheckResult;
|
|
6598
6591
|
}
|
|
@@ -6623,9 +6616,9 @@ var OcrStrategySampler = class {
|
|
|
6623
6616
|
model,
|
|
6624
6617
|
options
|
|
6625
6618
|
);
|
|
6626
|
-
if (sampleResult.
|
|
6619
|
+
if (sampleResult.foundKorean) {
|
|
6627
6620
|
this.logger.info(
|
|
6628
|
-
`[OcrStrategySampler] Korean
|
|
6621
|
+
`[OcrStrategySampler] Korean document detected on page ${sampleResult.koreanPageNo} \u2192 VLM strategy`
|
|
6629
6622
|
);
|
|
6630
6623
|
const detectedLanguages2 = this.aggregateLanguages(
|
|
6631
6624
|
sampleResult.languageFrequency
|
|
@@ -6633,13 +6626,13 @@ var OcrStrategySampler = class {
|
|
|
6633
6626
|
return {
|
|
6634
6627
|
method: "vlm",
|
|
6635
6628
|
detectedLanguages: detectedLanguages2,
|
|
6636
|
-
reason: `Korean
|
|
6629
|
+
reason: `Korean document detected on page ${sampleResult.koreanPageNo}`,
|
|
6637
6630
|
sampledPages: sampleResult.sampledCount,
|
|
6638
6631
|
totalPages: renderResult.pageCount
|
|
6639
6632
|
};
|
|
6640
6633
|
}
|
|
6641
6634
|
this.logger.info(
|
|
6642
|
-
"[OcrStrategySampler] No Korean
|
|
6635
|
+
"[OcrStrategySampler] No Korean language detected \u2192 ocrmac strategy"
|
|
6643
6636
|
);
|
|
6644
6637
|
const detectedLanguages = this.aggregateLanguages(
|
|
6645
6638
|
sampleResult.languageFrequency
|
|
@@ -6647,13 +6640,13 @@ var OcrStrategySampler = class {
|
|
|
6647
6640
|
return {
|
|
6648
6641
|
method: "ocrmac",
|
|
6649
6642
|
detectedLanguages,
|
|
6650
|
-
reason: `No Korean
|
|
6643
|
+
reason: `No Korean language detected in ${sampleResult.sampledCount} sampled pages`,
|
|
6651
6644
|
sampledPages: sampleResult.sampledCount,
|
|
6652
6645
|
totalPages: renderResult.pageCount
|
|
6653
6646
|
};
|
|
6654
6647
|
}
|
|
6655
6648
|
/**
|
|
6656
|
-
* Sample pages for Korean
|
|
6649
|
+
* Sample pages for Korean language detection with early exit.
|
|
6657
6650
|
*/
|
|
6658
6651
|
async samplePages(sampleIndices, pageFiles, model, options) {
|
|
6659
6652
|
const languageFrequency = /* @__PURE__ */ new Map();
|
|
@@ -6669,72 +6662,52 @@ var OcrStrategySampler = class {
|
|
|
6669
6662
|
for (const lang of pageAnalysis.detectedLanguages) {
|
|
6670
6663
|
languageFrequency.set(lang, (languageFrequency.get(lang) ?? 0) + 1);
|
|
6671
6664
|
}
|
|
6672
|
-
if (pageAnalysis.
|
|
6665
|
+
if (pageAnalysis.detectedLanguages.includes("ko-KR")) {
|
|
6673
6666
|
return {
|
|
6674
|
-
|
|
6675
|
-
|
|
6667
|
+
foundKorean: true,
|
|
6668
|
+
koreanPageNo: idx + 1,
|
|
6676
6669
|
sampledCount: i + 1,
|
|
6677
6670
|
languageFrequency
|
|
6678
6671
|
};
|
|
6679
6672
|
}
|
|
6680
6673
|
}
|
|
6681
6674
|
return {
|
|
6682
|
-
|
|
6675
|
+
foundKorean: false,
|
|
6683
6676
|
sampledCount: sampleIndices.length,
|
|
6684
6677
|
languageFrequency
|
|
6685
6678
|
};
|
|
6686
6679
|
}
|
|
6687
6680
|
/**
|
|
6688
|
-
* Pre-check for
|
|
6689
|
-
* Extracts full document text in a single process and checks at document level
|
|
6690
|
-
*
|
|
6691
|
-
* - Hangul + Hanja (anywhere in document) → VLM (confirmed Korean-Hanja mix)
|
|
6692
|
-
* - Hangul only → ocrmac with ko-KR (confirmed Korean)
|
|
6681
|
+
* Pre-check for Korean text in PDF text layer using pdftotext.
|
|
6682
|
+
* Extracts full document text in a single process and checks at document level:
|
|
6683
|
+
* - Hangul anywhere in document → VLM (confirmed Korean document)
|
|
6693
6684
|
* - No Hangul (English, Japanese, etc.) → null (delegates to VLM for language detection)
|
|
6694
6685
|
*/
|
|
6695
|
-
async
|
|
6686
|
+
async preCheckKoreanFromTextLayer(pdfPath) {
|
|
6696
6687
|
try {
|
|
6697
6688
|
const totalPages = await this.textExtractor.getPageCount(pdfPath);
|
|
6698
6689
|
if (totalPages === 0) return null;
|
|
6699
6690
|
const fullText = await this.textExtractor.extractFullText(pdfPath);
|
|
6700
6691
|
if (fullText.trim().length === 0) {
|
|
6701
6692
|
this.logger.debug(
|
|
6702
|
-
"[OcrStrategySampler] No
|
|
6693
|
+
"[OcrStrategySampler] No text in text layer, falling back to VLM sampling"
|
|
6703
6694
|
);
|
|
6704
6695
|
return null;
|
|
6705
6696
|
}
|
|
6706
6697
|
const hasHangul = HANGUL_REGEX.test(fullText);
|
|
6707
|
-
const hasHanja = CJK_REGEX.test(fullText);
|
|
6708
6698
|
if (!hasHangul) {
|
|
6709
6699
|
this.logger.debug(
|
|
6710
6700
|
"[OcrStrategySampler] No Hangul in text layer, falling back to VLM sampling"
|
|
6711
6701
|
);
|
|
6712
6702
|
return null;
|
|
6713
6703
|
}
|
|
6714
|
-
if (hasHanja) {
|
|
6715
|
-
const pageTextArray = fullText.split("\f");
|
|
6716
|
-
const koreanHanjaMixPages = pageTextArray.flatMap(
|
|
6717
|
-
(text, i) => CJK_REGEX.test(text) ? [i + 1] : []
|
|
6718
|
-
);
|
|
6719
|
-
this.logger.info(
|
|
6720
|
-
`[OcrStrategySampler] Hangul-Hanja mix detected in text layer \u2192 VLM strategy (${koreanHanjaMixPages.length} pages with Hanja)`
|
|
6721
|
-
);
|
|
6722
|
-
return {
|
|
6723
|
-
method: "vlm",
|
|
6724
|
-
detectedLanguages: ["ko-KR"],
|
|
6725
|
-
reason: "Hangul-Hanja mix found in PDF text layer",
|
|
6726
|
-
koreanHanjaMixPages,
|
|
6727
|
-
sampledPages: totalPages,
|
|
6728
|
-
totalPages
|
|
6729
|
-
};
|
|
6730
|
-
}
|
|
6731
6704
|
this.logger.info(
|
|
6732
|
-
"[OcrStrategySampler]
|
|
6705
|
+
"[OcrStrategySampler] Korean text detected in text layer \u2192 VLM strategy"
|
|
6733
6706
|
);
|
|
6734
6707
|
return {
|
|
6735
|
-
method: "
|
|
6708
|
+
method: "vlm",
|
|
6736
6709
|
detectedLanguages: ["ko-KR"],
|
|
6737
|
-
reason: `
|
|
6710
|
+
reason: `Korean text found in PDF text layer (${totalPages} pages checked)`,
|
|
6738
6711
|
sampledPages: totalPages,
|
|
6739
6712
|
totalPages
|
|
6740
6713
|
};
|
|
@@ -6775,21 +6748,24 @@ var OcrStrategySampler = class {
|
|
|
6775
6748
|
);
|
|
6776
6749
|
}
|
|
6777
6750
|
/**
|
|
6778
|
-
* Analyze a single sample page for
|
|
6751
|
+
* Analyze a single sample page for language.
|
|
6779
6752
|
* Normalizes raw VLM language responses to valid BCP 47 tags, filtering out invalid ones.
|
|
6780
6753
|
*
|
|
6781
|
-
* @returns Object with
|
|
6754
|
+
* @returns Object with normalized detected languages
|
|
6782
6755
|
*/
|
|
6783
6756
|
async analyzeSamplePage(pageFile, pageNo, model, options) {
|
|
6784
6757
|
this.logger.debug(
|
|
6785
|
-
`[OcrStrategySampler] Analyzing page ${pageNo} for
|
|
6758
|
+
`[OcrStrategySampler] Analyzing page ${pageNo} for language...`
|
|
6786
6759
|
);
|
|
6787
6760
|
const imageData = new Uint8Array((0, import_node_fs12.readFileSync)(pageFile));
|
|
6788
6761
|
const messages = [
|
|
6789
6762
|
{
|
|
6790
6763
|
role: "user",
|
|
6791
6764
|
content: [
|
|
6792
|
-
{
|
|
6765
|
+
{
|
|
6766
|
+
type: "text",
|
|
6767
|
+
text: KOREAN_DOCUMENT_DETECTION_PROMPT
|
|
6768
|
+
},
|
|
6793
6769
|
{
|
|
6794
6770
|
type: "image",
|
|
6795
6771
|
image: imageData,
|
|
@@ -6799,7 +6775,7 @@ var OcrStrategySampler = class {
|
|
|
6799
6775
|
}
|
|
6800
6776
|
];
|
|
6801
6777
|
const result = await LLMCaller.callVision({
|
|
6802
|
-
schema:
|
|
6778
|
+
schema: koreanDocumentDetectionSchema,
|
|
6803
6779
|
messages,
|
|
6804
6780
|
primaryModel: model,
|
|
6805
6781
|
fallbackModel: options?.fallbackModel,
|
|
@@ -6807,7 +6783,7 @@ var OcrStrategySampler = class {
|
|
|
6807
6783
|
temperature: options?.temperature ?? 0,
|
|
6808
6784
|
abortSignal: options?.abortSignal,
|
|
6809
6785
|
component: "OcrStrategySampler",
|
|
6810
|
-
phase: "korean-
|
|
6786
|
+
phase: "korean-document-detection"
|
|
6811
6787
|
});
|
|
6812
6788
|
if (options?.aggregator) {
|
|
6813
6789
|
options.aggregator.track(result.usage);
|
|
@@ -6815,10 +6791,9 @@ var OcrStrategySampler = class {
|
|
|
6815
6791
|
const output = result.output;
|
|
6816
6792
|
const normalizedLanguages = output.detectedLanguages.map(import_model.normalizeToBcp47).filter((tag) => tag !== null);
|
|
6817
6793
|
this.logger.debug(
|
|
6818
|
-
`[OcrStrategySampler] Page ${pageNo}:
|
|
6794
|
+
`[OcrStrategySampler] Page ${pageNo}: detectedLanguages=${normalizedLanguages.join(",")}`
|
|
6819
6795
|
);
|
|
6820
6796
|
return {
|
|
6821
|
-
hasKoreanHanjaMix: output.hasKoreanHanjaMix,
|
|
6822
6797
|
detectedLanguages: normalizedLanguages
|
|
6823
6798
|
};
|
|
6824
6799
|
}
|
|
@@ -7067,16 +7042,7 @@ var VlmTextCorrector = class {
|
|
|
7067
7042
|
pagesFailed: 0
|
|
7068
7043
|
};
|
|
7069
7044
|
}
|
|
7070
|
-
|
|
7071
|
-
if (options?.koreanHanjaMixPages && options.koreanHanjaMixPages.length > 0) {
|
|
7072
|
-
const hanjaSet = new Set(options.koreanHanjaMixPages);
|
|
7073
|
-
pageNumbers = allPageNumbers.filter((p) => hanjaSet.has(p));
|
|
7074
|
-
this.logger.info(
|
|
7075
|
-
`[VlmTextCorrector] Filtering to ${pageNumbers.length} Korean-Hanja mix pages out of ${allPageNumbers.length} total`
|
|
7076
|
-
);
|
|
7077
|
-
} else {
|
|
7078
|
-
pageNumbers = allPageNumbers;
|
|
7079
|
-
}
|
|
7045
|
+
const pageNumbers = allPageNumbers;
|
|
7080
7046
|
const concurrency = options?.concurrency ?? DEFAULT_CONCURRENCY;
|
|
7081
7047
|
this.logger.info(
|
|
7082
7048
|
`[VlmTextCorrector] Processing ${pageNumbers.length} pages (concurrency: ${concurrency})...`
|
|
@@ -7281,7 +7247,7 @@ var VlmConversionPipeline = class {
|
|
|
7281
7247
|
* Wrap the original callback with VLM text correction.
|
|
7282
7248
|
* Returns a new callback that runs VLM correction before calling the original.
|
|
7283
7249
|
*/
|
|
7284
|
-
wrapCallback(pdfPath, options, originalCallback, abortSignal, detectedLanguages
|
|
7250
|
+
wrapCallback(pdfPath, options, originalCallback, abortSignal, detectedLanguages) {
|
|
7285
7251
|
if (!options.vlmProcessorModel) {
|
|
7286
7252
|
throw new Error("vlmProcessorModel is required when OCR strategy is VLM");
|
|
7287
7253
|
}
|
|
@@ -7313,8 +7279,7 @@ var VlmConversionPipeline = class {
|
|
|
7313
7279
|
abortSignal,
|
|
7314
7280
|
onTokenUsage: options.onTokenUsage,
|
|
7315
7281
|
documentLanguages: detectedLanguages,
|
|
7316
|
-
pageTexts
|
|
7317
|
-
koreanHanjaMixPages
|
|
7282
|
+
pageTexts
|
|
7318
7283
|
}
|
|
7319
7284
|
);
|
|
7320
7285
|
await originalCallback(outputDir);
|
|
@@ -7466,8 +7431,7 @@ var PDFConverter = class {
|
|
|
7466
7431
|
trackedOptions,
|
|
7467
7432
|
postCorrectionCallback,
|
|
7468
7433
|
abortSignal,
|
|
7469
|
-
strategy.detectedLanguages
|
|
7470
|
-
strategy.koreanHanjaMixPages
|
|
7434
|
+
strategy.detectedLanguages
|
|
7471
7435
|
);
|
|
7472
7436
|
const vlmOptions = strategy.detectedLanguages ? { ...trackedOptions, ocr_lang: strategy.detectedLanguages } : trackedOptions;
|
|
7473
7437
|
await this.convert(
|
|
@@ -8002,7 +7966,7 @@ var META_DESCRIPTION_PATTERNS_EN = [
|
|
|
8002
7966
|
var REPETITIVE_PATTERN_RATIO_THRESHOLD = 0.3;
|
|
8003
7967
|
var REPETITIVE_PATTERN_MIN_REPEATS = 5;
|
|
8004
7968
|
var HANGUL_REGEX2 = /[\uAC00-\uD7AF\u1100-\u11FF]/g;
|
|
8005
|
-
var
|
|
7969
|
+
var CJK_REGEX = /[\u4E00-\u9FFF]/g;
|
|
8006
7970
|
var VlmResponseValidator = class {
|
|
8007
7971
|
/**
|
|
8008
7972
|
* Validate VLM page result quality.
|
|
@@ -8070,7 +8034,7 @@ var VlmResponseValidator = class {
|
|
|
8070
8034
|
return null;
|
|
8071
8035
|
}
|
|
8072
8036
|
const hangulCount = allContent.match(HANGUL_REGEX2)?.length ?? 0;
|
|
8073
|
-
const cjkCount = allContent.match(
|
|
8037
|
+
const cjkCount = allContent.match(CJK_REGEX)?.length ?? 0;
|
|
8074
8038
|
const koreanCjkCount = hangulCount + cjkCount;
|
|
8075
8039
|
const ratio = koreanCjkCount / nonWhitespace.length;
|
|
8076
8040
|
if (ratio < KOREAN_SCRIPT_RATIO_THRESHOLD) {
|