@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/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-hanja-mix-prompt.ts
6549
- var KOREAN_HANJA_MIX_PROMPT = `Look at this page image carefully. Does it contain any Hanja (\u6F22\u5B57/Chinese characters) mixed with Korean text?
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
- Hanja examples: \u907A\u8E5F, \u767C\u6398, \u8ABF\u67FB, \u5831\u544A\u66F8, \u6587\u5316\u8CA1
6552
- Note: Hanja are Chinese characters used in Korean documents, different from modern Korean (\uD55C\uAE00).
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 koreanHanjaMixSchema = import_v42.z.object({
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-Hanja mix detection
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.preCheckHanjaFromTextLayer(pdfPath);
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.foundMix) {
6619
+ if (sampleResult.foundKorean) {
6627
6620
  this.logger.info(
6628
- `[OcrStrategySampler] Korean-Hanja mix detected on page ${sampleResult.mixPageNo} \u2192 VLM strategy`
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-Hanja mix detected on page ${sampleResult.mixPageNo}`,
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-Hanja mix detected \u2192 ocrmac strategy"
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-Hanja mix detected in ${sampleResult.sampledCount} sampled pages`,
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-Hanja mix detection with early exit.
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.hasKoreanHanjaMix) {
6665
+ if (pageAnalysis.detectedLanguages.includes("ko-KR")) {
6673
6666
  return {
6674
- foundMix: true,
6675
- mixPageNo: idx + 1,
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
- foundMix: false,
6675
+ foundKorean: false,
6683
6676
  sampledCount: sampleIndices.length,
6684
6677
  languageFrequency
6685
6678
  };
6686
6679
  }
6687
6680
  /**
6688
- * Pre-check for Hangul-Hanja mix in PDF text layer using pdftotext.
6689
- * Extracts full document text in a single process and checks at document level.
6690
- * Only makes a definitive decision for Korean (Hangul) documents:
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 preCheckHanjaFromTextLayer(pdfPath) {
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 Hangul in text layer, falling back to VLM sampling"
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] No Hangul-Hanja mix in text layer \u2192 ocrmac strategy"
6705
+ "[OcrStrategySampler] Korean text detected in text layer \u2192 VLM strategy"
6733
6706
  );
6734
6707
  return {
6735
- method: "ocrmac",
6708
+ method: "vlm",
6736
6709
  detectedLanguages: ["ko-KR"],
6737
- reason: `No Hangul-Hanja mix in PDF text layer (${totalPages} pages checked)`,
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 Korean-Hanja mixed script and primary language.
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 Korean-Hanja detection result and normalized detected languages
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 Korean-Hanja mix and language...`
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
- { type: "text", text: KOREAN_HANJA_MIX_PROMPT },
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: koreanHanjaMixSchema,
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-hanja-mix-detection"
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}: hasKoreanHanjaMix=${output.hasKoreanHanjaMix}, detectedLanguages=${normalizedLanguages.join(",")}`
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
- let pageNumbers;
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, koreanHanjaMixPages) {
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 CJK_REGEX2 = /[\u4E00-\u9FFF]/g;
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(CJK_REGEX2)?.length ?? 0;
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) {