@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.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-hanja-mix-prompt.ts
6529
- var KOREAN_HANJA_MIX_PROMPT = `Look at this page image carefully. Does it contain any Hanja (\u6F22\u5B57/Chinese characters) mixed with Korean text?
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
- Hanja examples: \u907A\u8E5F, \u767C\u6398, \u8ABF\u67FB, \u5831\u544A\u66F8, \u6587\u5316\u8CA1
6532
- Note: Hanja are Chinese characters used in Korean documents, different from modern Korean (\uD55C\uAE00).
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 koreanHanjaMixSchema = z3.object({
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-Hanja mix detection
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.preCheckHanjaFromTextLayer(pdfPath);
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.foundMix) {
6599
+ if (sampleResult.foundKorean) {
6607
6600
  this.logger.info(
6608
- `[OcrStrategySampler] Korean-Hanja mix detected on page ${sampleResult.mixPageNo} \u2192 VLM strategy`
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-Hanja mix detected on page ${sampleResult.mixPageNo}`,
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-Hanja mix detected \u2192 ocrmac strategy"
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-Hanja mix detected in ${sampleResult.sampledCount} sampled pages`,
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-Hanja mix detection with early exit.
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.hasKoreanHanjaMix) {
6645
+ if (pageAnalysis.detectedLanguages.includes("ko-KR")) {
6653
6646
  return {
6654
- foundMix: true,
6655
- mixPageNo: idx + 1,
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
- foundMix: false,
6655
+ foundKorean: false,
6663
6656
  sampledCount: sampleIndices.length,
6664
6657
  languageFrequency
6665
6658
  };
6666
6659
  }
6667
6660
  /**
6668
- * Pre-check for Hangul-Hanja mix in PDF text layer using pdftotext.
6669
- * Extracts full document text in a single process and checks at document level.
6670
- * Only makes a definitive decision for Korean (Hangul) documents:
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 preCheckHanjaFromTextLayer(pdfPath) {
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 Hangul in text layer, falling back to VLM sampling"
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] No Hangul-Hanja mix in text layer \u2192 ocrmac strategy"
6685
+ "[OcrStrategySampler] Korean text detected in text layer \u2192 VLM strategy"
6713
6686
  );
6714
6687
  return {
6715
- method: "ocrmac",
6688
+ method: "vlm",
6716
6689
  detectedLanguages: ["ko-KR"],
6717
- reason: `No Hangul-Hanja mix in PDF text layer (${totalPages} pages checked)`,
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 Korean-Hanja mixed script and primary language.
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 Korean-Hanja detection result and normalized detected languages
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 Korean-Hanja mix and language...`
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
- { type: "text", text: KOREAN_HANJA_MIX_PROMPT },
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: koreanHanjaMixSchema,
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-hanja-mix-detection"
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}: hasKoreanHanjaMix=${output.hasKoreanHanjaMix}, detectedLanguages=${normalizedLanguages.join(",")}`
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
- let pageNumbers;
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, koreanHanjaMixPages) {
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 CJK_REGEX2 = /[\u4E00-\u9FFF]/g;
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(CJK_REGEX2)?.length ?? 0;
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) {