@heripo/document-processor 0.1.5 → 0.1.7
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 +342 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -2
- package/dist/index.d.ts +88 -2
- package/dist/index.js +341 -15
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LoggerMethods } from '@heripo/logger';
|
|
2
|
-
import { DoclingDocument, DocumentProcessResult, Caption, PageRange, DoclingTextItem, DoclingPictureItem, DoclingTableItem, DoclingGroupItem, ProcessedImage, ProcessedTable, ProcessedFootnote, Chapter } from '@heripo/model';
|
|
2
|
+
import { DoclingDocument, HanjaAssessment, DocumentProcessResult, Caption, PageRange, DoclingTextItem, DoclingPictureItem, DoclingTableItem, DoclingGroupItem, ProcessedImage, ProcessedTable, ProcessedFootnote, Chapter } from '@heripo/model';
|
|
3
3
|
import { LanguageModel } from 'ai';
|
|
4
4
|
import { LLMTokenUsageAggregator, ExtendedTokenUsage } from '@heripo/shared';
|
|
5
5
|
import { z } from 'zod';
|
|
@@ -43,6 +43,11 @@ interface DocumentProcessorOptions {
|
|
|
43
43
|
* Falls back to 'fallbackModel' if not provided.
|
|
44
44
|
*/
|
|
45
45
|
captionParserModel?: LanguageModel;
|
|
46
|
+
/**
|
|
47
|
+
* Model for HanjaQualitySampler - evaluates KCJ character quality in OCR output.
|
|
48
|
+
* Requires vision capabilities. Falls back to 'fallbackModel' if not provided.
|
|
49
|
+
*/
|
|
50
|
+
hanjaQualitySamplerModel?: LanguageModel;
|
|
46
51
|
/**
|
|
47
52
|
* Batch size for TextCleaner text normalization (synchronous processing)
|
|
48
53
|
*/
|
|
@@ -132,6 +137,7 @@ declare class DocumentProcessor {
|
|
|
132
137
|
private readonly validatorModel;
|
|
133
138
|
private readonly visionTocExtractorModel;
|
|
134
139
|
private readonly captionParserModel;
|
|
140
|
+
private readonly hanjaQualitySamplerModel;
|
|
135
141
|
private readonly textCleanerBatchSize;
|
|
136
142
|
private readonly captionParserBatchSize;
|
|
137
143
|
private readonly captionValidatorBatchSize;
|
|
@@ -157,6 +163,18 @@ declare class DocumentProcessor {
|
|
|
157
163
|
* @throws {Error} with name 'AbortError' if aborted
|
|
158
164
|
*/
|
|
159
165
|
private checkAborted;
|
|
166
|
+
/**
|
|
167
|
+
* Assess the quality of KCJ (Hanja) character recognition in the OCR output.
|
|
168
|
+
*
|
|
169
|
+
* Samples pages containing KCJ characters and uses Vision LLM to compare
|
|
170
|
+
* the OCR text against the original page images. Returns an assessment
|
|
171
|
+
* indicating whether the document should be re-parsed with VLM pipeline.
|
|
172
|
+
*
|
|
173
|
+
* @param doclingDoc - Original document extracted from Docling SDK
|
|
174
|
+
* @param outputPath - Path containing pages subdirectory (pages/page_0.png, etc.)
|
|
175
|
+
* @returns Assessment result with severity and recommendation
|
|
176
|
+
*/
|
|
177
|
+
assessHanjaQuality(doclingDoc: DoclingDocument, outputPath: string): Promise<HanjaAssessment>;
|
|
160
178
|
/**
|
|
161
179
|
* Converts DoclingDocument to ProcessedDocument with token usage tracking.
|
|
162
180
|
*
|
|
@@ -232,6 +250,14 @@ declare class DocumentProcessor {
|
|
|
232
250
|
* 5. TocExtractor - LLM-based structured extraction
|
|
233
251
|
*/
|
|
234
252
|
private extractTableOfContents;
|
|
253
|
+
/**
|
|
254
|
+
* Extract the maximum page number from TOC markdown
|
|
255
|
+
*
|
|
256
|
+
* Parses page numbers from dot-leader patterns (e.g., "..... 175")
|
|
257
|
+
* to detect compiled volume scenarios where TOC page numbers exceed
|
|
258
|
+
* the sub-document's page count.
|
|
259
|
+
*/
|
|
260
|
+
private extractMaxPageNumber;
|
|
235
261
|
/**
|
|
236
262
|
* Process resource captions (for images and tables)
|
|
237
263
|
*
|
|
@@ -1482,4 +1508,64 @@ declare class ChapterConverter {
|
|
|
1482
1508
|
private buildChapterMap;
|
|
1483
1509
|
}
|
|
1484
1510
|
|
|
1485
|
-
|
|
1511
|
+
/**
|
|
1512
|
+
* HanjaQualitySampler
|
|
1513
|
+
*
|
|
1514
|
+
* Evaluates the quality of KCJ (Chinese/Japanese/Korean) character recognition
|
|
1515
|
+
* in OCR-processed documents. Samples a subset of pages containing KCJ text and
|
|
1516
|
+
* uses Vision LLM to compare the OCR output against the original page images.
|
|
1517
|
+
*
|
|
1518
|
+
* This is used to determine whether a document should be re-parsed using the
|
|
1519
|
+
* VLM pipeline for better KCJ character accuracy.
|
|
1520
|
+
*/
|
|
1521
|
+
declare class HanjaQualitySampler extends VisionLLMComponent {
|
|
1522
|
+
constructor(logger: LoggerMethods, model: LanguageModel, outputPath: string, maxRetries?: number, fallbackModel?: LanguageModel, aggregator?: LLMTokenUsageAggregator, abortSignal?: AbortSignal);
|
|
1523
|
+
/**
|
|
1524
|
+
* Assess the quality of KCJ character recognition in the document
|
|
1525
|
+
*
|
|
1526
|
+
* @param doclingDoc - DoclingDocument to assess
|
|
1527
|
+
* @returns Assessment result indicating whether VLM re-parse is needed
|
|
1528
|
+
*/
|
|
1529
|
+
assess(doclingDoc: DoclingDocument): Promise<HanjaAssessment>;
|
|
1530
|
+
/**
|
|
1531
|
+
* Get the eligible page range after trimming front/back edges.
|
|
1532
|
+
* Returns cutoff values where eligible pages satisfy: pageNo > frontCutoff && pageNo <= backCutoff
|
|
1533
|
+
*/
|
|
1534
|
+
private getEligiblePageRange;
|
|
1535
|
+
/**
|
|
1536
|
+
* Get page numbers that are "image-only" (contain pictures but minimal text).
|
|
1537
|
+
* These pages (e.g., photo plates/도판) are unsuitable for KCJ quality assessment.
|
|
1538
|
+
*/
|
|
1539
|
+
private getImageOnlyPages;
|
|
1540
|
+
/**
|
|
1541
|
+
* Get all pages with substantial text content.
|
|
1542
|
+
* Pages with fewer characters than MIN_TEXT_LENGTH (e.g., page numbers, headers only) are excluded.
|
|
1543
|
+
*/
|
|
1544
|
+
private getTextPages;
|
|
1545
|
+
/**
|
|
1546
|
+
* Select sample pages with highest text density.
|
|
1547
|
+
* Prefers pages in the eligible range (not edge-trimmed) and not image-only.
|
|
1548
|
+
* Falls back to all text pages if filtering leaves too few candidates.
|
|
1549
|
+
*/
|
|
1550
|
+
private selectSamplePages;
|
|
1551
|
+
/**
|
|
1552
|
+
* Evaluate sampled pages using Vision LLM
|
|
1553
|
+
*/
|
|
1554
|
+
private evaluatePages;
|
|
1555
|
+
/**
|
|
1556
|
+
* Evaluate a single page by comparing OCR text against the page image
|
|
1557
|
+
*/
|
|
1558
|
+
private evaluateSinglePage;
|
|
1559
|
+
/**
|
|
1560
|
+
* Aggregate individual page results into final assessment
|
|
1561
|
+
*/
|
|
1562
|
+
private aggregateResults;
|
|
1563
|
+
/**
|
|
1564
|
+
* Get page number from a text item
|
|
1565
|
+
*/
|
|
1566
|
+
private getPageNo;
|
|
1567
|
+
protected buildSystemPrompt(): string;
|
|
1568
|
+
protected buildUserPrompt(ocrText: string): string;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
export { BaseLLMComponent, type BaseLLMComponentOptions, BaseValidator, type BaseValidatorOptions, CONTINUATION_MARKERS, CaptionParseError, CaptionParser, type CaptionParserOptions, CaptionValidationError, CaptionValidator, type CaptionValidatorOptions, ChapterConverter, DocumentProcessor, type DocumentProcessorOptions, HanjaQualitySampler, type ImageContent, PAGE_NUMBER_PATTERN, PagePattern, PageRangeParseError, PageRangeParser, type PageSizeGroup, TOC_KEYWORDS, TextLLMComponent, type TocAreaResult, type TocContentValidationResult, TocContentValidationSchema, TocContentValidator, type TocContentValidatorOptions, type TocEntry, TocEntrySchema, TocExtractError, TocExtractor, type TocExtractorOptions, TocFinder, type TocFinderOptions, TocNotFoundError, TocParseError, type TocResponse, TocResponseSchema, VisionLLMComponent, type VisionLLMComponentOptions, type VisionTocExtractionResult, VisionTocExtractionSchema, VisionTocExtractor, type VisionTocExtractorOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { LoggerMethods } from '@heripo/logger';
|
|
2
|
-
import { DoclingDocument, DocumentProcessResult, Caption, PageRange, DoclingTextItem, DoclingPictureItem, DoclingTableItem, DoclingGroupItem, ProcessedImage, ProcessedTable, ProcessedFootnote, Chapter } from '@heripo/model';
|
|
2
|
+
import { DoclingDocument, HanjaAssessment, DocumentProcessResult, Caption, PageRange, DoclingTextItem, DoclingPictureItem, DoclingTableItem, DoclingGroupItem, ProcessedImage, ProcessedTable, ProcessedFootnote, Chapter } from '@heripo/model';
|
|
3
3
|
import { LanguageModel } from 'ai';
|
|
4
4
|
import { LLMTokenUsageAggregator, ExtendedTokenUsage } from '@heripo/shared';
|
|
5
5
|
import { z } from 'zod';
|
|
@@ -43,6 +43,11 @@ interface DocumentProcessorOptions {
|
|
|
43
43
|
* Falls back to 'fallbackModel' if not provided.
|
|
44
44
|
*/
|
|
45
45
|
captionParserModel?: LanguageModel;
|
|
46
|
+
/**
|
|
47
|
+
* Model for HanjaQualitySampler - evaluates KCJ character quality in OCR output.
|
|
48
|
+
* Requires vision capabilities. Falls back to 'fallbackModel' if not provided.
|
|
49
|
+
*/
|
|
50
|
+
hanjaQualitySamplerModel?: LanguageModel;
|
|
46
51
|
/**
|
|
47
52
|
* Batch size for TextCleaner text normalization (synchronous processing)
|
|
48
53
|
*/
|
|
@@ -132,6 +137,7 @@ declare class DocumentProcessor {
|
|
|
132
137
|
private readonly validatorModel;
|
|
133
138
|
private readonly visionTocExtractorModel;
|
|
134
139
|
private readonly captionParserModel;
|
|
140
|
+
private readonly hanjaQualitySamplerModel;
|
|
135
141
|
private readonly textCleanerBatchSize;
|
|
136
142
|
private readonly captionParserBatchSize;
|
|
137
143
|
private readonly captionValidatorBatchSize;
|
|
@@ -157,6 +163,18 @@ declare class DocumentProcessor {
|
|
|
157
163
|
* @throws {Error} with name 'AbortError' if aborted
|
|
158
164
|
*/
|
|
159
165
|
private checkAborted;
|
|
166
|
+
/**
|
|
167
|
+
* Assess the quality of KCJ (Hanja) character recognition in the OCR output.
|
|
168
|
+
*
|
|
169
|
+
* Samples pages containing KCJ characters and uses Vision LLM to compare
|
|
170
|
+
* the OCR text against the original page images. Returns an assessment
|
|
171
|
+
* indicating whether the document should be re-parsed with VLM pipeline.
|
|
172
|
+
*
|
|
173
|
+
* @param doclingDoc - Original document extracted from Docling SDK
|
|
174
|
+
* @param outputPath - Path containing pages subdirectory (pages/page_0.png, etc.)
|
|
175
|
+
* @returns Assessment result with severity and recommendation
|
|
176
|
+
*/
|
|
177
|
+
assessHanjaQuality(doclingDoc: DoclingDocument, outputPath: string): Promise<HanjaAssessment>;
|
|
160
178
|
/**
|
|
161
179
|
* Converts DoclingDocument to ProcessedDocument with token usage tracking.
|
|
162
180
|
*
|
|
@@ -232,6 +250,14 @@ declare class DocumentProcessor {
|
|
|
232
250
|
* 5. TocExtractor - LLM-based structured extraction
|
|
233
251
|
*/
|
|
234
252
|
private extractTableOfContents;
|
|
253
|
+
/**
|
|
254
|
+
* Extract the maximum page number from TOC markdown
|
|
255
|
+
*
|
|
256
|
+
* Parses page numbers from dot-leader patterns (e.g., "..... 175")
|
|
257
|
+
* to detect compiled volume scenarios where TOC page numbers exceed
|
|
258
|
+
* the sub-document's page count.
|
|
259
|
+
*/
|
|
260
|
+
private extractMaxPageNumber;
|
|
235
261
|
/**
|
|
236
262
|
* Process resource captions (for images and tables)
|
|
237
263
|
*
|
|
@@ -1482,4 +1508,64 @@ declare class ChapterConverter {
|
|
|
1482
1508
|
private buildChapterMap;
|
|
1483
1509
|
}
|
|
1484
1510
|
|
|
1485
|
-
|
|
1511
|
+
/**
|
|
1512
|
+
* HanjaQualitySampler
|
|
1513
|
+
*
|
|
1514
|
+
* Evaluates the quality of KCJ (Chinese/Japanese/Korean) character recognition
|
|
1515
|
+
* in OCR-processed documents. Samples a subset of pages containing KCJ text and
|
|
1516
|
+
* uses Vision LLM to compare the OCR output against the original page images.
|
|
1517
|
+
*
|
|
1518
|
+
* This is used to determine whether a document should be re-parsed using the
|
|
1519
|
+
* VLM pipeline for better KCJ character accuracy.
|
|
1520
|
+
*/
|
|
1521
|
+
declare class HanjaQualitySampler extends VisionLLMComponent {
|
|
1522
|
+
constructor(logger: LoggerMethods, model: LanguageModel, outputPath: string, maxRetries?: number, fallbackModel?: LanguageModel, aggregator?: LLMTokenUsageAggregator, abortSignal?: AbortSignal);
|
|
1523
|
+
/**
|
|
1524
|
+
* Assess the quality of KCJ character recognition in the document
|
|
1525
|
+
*
|
|
1526
|
+
* @param doclingDoc - DoclingDocument to assess
|
|
1527
|
+
* @returns Assessment result indicating whether VLM re-parse is needed
|
|
1528
|
+
*/
|
|
1529
|
+
assess(doclingDoc: DoclingDocument): Promise<HanjaAssessment>;
|
|
1530
|
+
/**
|
|
1531
|
+
* Get the eligible page range after trimming front/back edges.
|
|
1532
|
+
* Returns cutoff values where eligible pages satisfy: pageNo > frontCutoff && pageNo <= backCutoff
|
|
1533
|
+
*/
|
|
1534
|
+
private getEligiblePageRange;
|
|
1535
|
+
/**
|
|
1536
|
+
* Get page numbers that are "image-only" (contain pictures but minimal text).
|
|
1537
|
+
* These pages (e.g., photo plates/도판) are unsuitable for KCJ quality assessment.
|
|
1538
|
+
*/
|
|
1539
|
+
private getImageOnlyPages;
|
|
1540
|
+
/**
|
|
1541
|
+
* Get all pages with substantial text content.
|
|
1542
|
+
* Pages with fewer characters than MIN_TEXT_LENGTH (e.g., page numbers, headers only) are excluded.
|
|
1543
|
+
*/
|
|
1544
|
+
private getTextPages;
|
|
1545
|
+
/**
|
|
1546
|
+
* Select sample pages with highest text density.
|
|
1547
|
+
* Prefers pages in the eligible range (not edge-trimmed) and not image-only.
|
|
1548
|
+
* Falls back to all text pages if filtering leaves too few candidates.
|
|
1549
|
+
*/
|
|
1550
|
+
private selectSamplePages;
|
|
1551
|
+
/**
|
|
1552
|
+
* Evaluate sampled pages using Vision LLM
|
|
1553
|
+
*/
|
|
1554
|
+
private evaluatePages;
|
|
1555
|
+
/**
|
|
1556
|
+
* Evaluate a single page by comparing OCR text against the page image
|
|
1557
|
+
*/
|
|
1558
|
+
private evaluateSinglePage;
|
|
1559
|
+
/**
|
|
1560
|
+
* Aggregate individual page results into final assessment
|
|
1561
|
+
*/
|
|
1562
|
+
private aggregateResults;
|
|
1563
|
+
/**
|
|
1564
|
+
* Get page number from a text item
|
|
1565
|
+
*/
|
|
1566
|
+
private getPageNo;
|
|
1567
|
+
protected buildSystemPrompt(): string;
|
|
1568
|
+
protected buildUserPrompt(ocrText: string): string;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
export { BaseLLMComponent, type BaseLLMComponentOptions, BaseValidator, type BaseValidatorOptions, CONTINUATION_MARKERS, CaptionParseError, CaptionParser, type CaptionParserOptions, CaptionValidationError, CaptionValidator, type CaptionValidatorOptions, ChapterConverter, DocumentProcessor, type DocumentProcessorOptions, HanjaQualitySampler, type ImageContent, PAGE_NUMBER_PATTERN, PagePattern, PageRangeParseError, PageRangeParser, type PageSizeGroup, TOC_KEYWORDS, TextLLMComponent, type TocAreaResult, type TocContentValidationResult, TocContentValidationSchema, TocContentValidator, type TocContentValidatorOptions, type TocEntry, TocEntrySchema, TocExtractError, TocExtractor, type TocExtractorOptions, TocFinder, type TocFinderOptions, TocNotFoundError, TocParseError, type TocResponse, TocResponseSchema, VisionLLMComponent, type VisionLLMComponentOptions, type VisionTocExtractionResult, VisionTocExtractionSchema, VisionTocExtractor, type VisionTocExtractorOptions };
|
package/dist/index.js
CHANGED
|
@@ -3346,6 +3346,283 @@ Remember: Look for SMALL numbers in MARGINS only. Ignore figure/table/photo numb
|
|
|
3346
3346
|
}
|
|
3347
3347
|
};
|
|
3348
3348
|
|
|
3349
|
+
// src/samplers/hanja-quality-sampler.ts
|
|
3350
|
+
import { z as z5 } from "zod";
|
|
3351
|
+
var MIN_TEXT_LENGTH = 100;
|
|
3352
|
+
var MAX_SAMPLE_PAGES = 10;
|
|
3353
|
+
var EDGE_TRIM_RATIO = 0.1;
|
|
3354
|
+
var IMAGE_PAGE_TEXT_THRESHOLD = 50;
|
|
3355
|
+
var CORRUPTION_THRESHOLD = 0.5;
|
|
3356
|
+
var MIN_SEVERE_CORRUPTED_COUNT = 3;
|
|
3357
|
+
var HanjaQualityResponseSchema = z5.object({
|
|
3358
|
+
isCorrupted: z5.boolean().describe(
|
|
3359
|
+
"Whether the KCJ (Hanja) characters in the OCR text are significantly corrupted compared to the page image"
|
|
3360
|
+
),
|
|
3361
|
+
corruptedCharCount: z5.number().int().nonnegative().describe(
|
|
3362
|
+
"Approximate number of corrupted or incorrectly recognized KCJ characters"
|
|
3363
|
+
),
|
|
3364
|
+
totalKcjCharCount: z5.number().int().nonnegative().describe("Total number of KCJ characters found in the OCR text"),
|
|
3365
|
+
explanation: z5.string().describe("Brief explanation of the assessment result")
|
|
3366
|
+
});
|
|
3367
|
+
var HanjaQualitySampler = class extends VisionLLMComponent {
|
|
3368
|
+
constructor(logger, model, outputPath, maxRetries, fallbackModel, aggregator, abortSignal) {
|
|
3369
|
+
super(
|
|
3370
|
+
logger,
|
|
3371
|
+
model,
|
|
3372
|
+
"HanjaQualitySampler",
|
|
3373
|
+
outputPath,
|
|
3374
|
+
{ maxRetries, abortSignal },
|
|
3375
|
+
fallbackModel,
|
|
3376
|
+
aggregator
|
|
3377
|
+
);
|
|
3378
|
+
}
|
|
3379
|
+
/**
|
|
3380
|
+
* Assess the quality of KCJ character recognition in the document
|
|
3381
|
+
*
|
|
3382
|
+
* @param doclingDoc - DoclingDocument to assess
|
|
3383
|
+
* @returns Assessment result indicating whether VLM re-parse is needed
|
|
3384
|
+
*/
|
|
3385
|
+
async assess(doclingDoc) {
|
|
3386
|
+
const totalPages = Object.keys(doclingDoc.pages).length;
|
|
3387
|
+
this.log("info", "Starting KCJ quality assessment...");
|
|
3388
|
+
const { frontCutoff, backCutoff } = this.getEligiblePageRange(totalPages);
|
|
3389
|
+
const imageOnlyPages = this.getImageOnlyPages(doclingDoc);
|
|
3390
|
+
this.log(
|
|
3391
|
+
"info",
|
|
3392
|
+
`Total pages: ${totalPages}, eligible range: (${frontCutoff}, ${backCutoff}], image-only pages excluded: ${imageOnlyPages.size}`
|
|
3393
|
+
);
|
|
3394
|
+
const textPages = this.getTextPages(doclingDoc);
|
|
3395
|
+
if (textPages.length === 0) {
|
|
3396
|
+
this.log("info", "No text pages found for assessment");
|
|
3397
|
+
return {
|
|
3398
|
+
needsVlmReparse: false,
|
|
3399
|
+
severity: "none",
|
|
3400
|
+
kcjPageCount: 0,
|
|
3401
|
+
sampledPageCount: 0,
|
|
3402
|
+
corruptedRatio: 0,
|
|
3403
|
+
reason: "No text pages found for assessment"
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
this.log(
|
|
3407
|
+
"info",
|
|
3408
|
+
`Found ${textPages.length} text pages (min length: ${MIN_TEXT_LENGTH})`
|
|
3409
|
+
);
|
|
3410
|
+
const sampled = this.selectSamplePages(
|
|
3411
|
+
textPages,
|
|
3412
|
+
totalPages,
|
|
3413
|
+
imageOnlyPages
|
|
3414
|
+
);
|
|
3415
|
+
this.log(
|
|
3416
|
+
"info",
|
|
3417
|
+
`Sampling ${sampled.length} pages: [${sampled.map((p) => p.pageNo).join(", ")}]`
|
|
3418
|
+
);
|
|
3419
|
+
const results = await this.evaluatePages(sampled);
|
|
3420
|
+
const assessment = this.aggregateResults(
|
|
3421
|
+
textPages.length,
|
|
3422
|
+
sampled.length,
|
|
3423
|
+
results
|
|
3424
|
+
);
|
|
3425
|
+
this.log(
|
|
3426
|
+
"info",
|
|
3427
|
+
`Assessment complete: severity=${assessment.severity}, corrupted=${assessment.corruptedRatio.toFixed(2)}, needsVlmReparse=${assessment.needsVlmReparse}`
|
|
3428
|
+
);
|
|
3429
|
+
return assessment;
|
|
3430
|
+
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Get the eligible page range after trimming front/back edges.
|
|
3433
|
+
* Returns cutoff values where eligible pages satisfy: pageNo > frontCutoff && pageNo <= backCutoff
|
|
3434
|
+
*/
|
|
3435
|
+
getEligiblePageRange(totalPages) {
|
|
3436
|
+
const trimCount = Math.ceil(totalPages * EDGE_TRIM_RATIO);
|
|
3437
|
+
return {
|
|
3438
|
+
frontCutoff: trimCount,
|
|
3439
|
+
backCutoff: totalPages - trimCount
|
|
3440
|
+
};
|
|
3441
|
+
}
|
|
3442
|
+
/**
|
|
3443
|
+
* Get page numbers that are "image-only" (contain pictures but minimal text).
|
|
3444
|
+
* These pages (e.g., photo plates/도판) are unsuitable for KCJ quality assessment.
|
|
3445
|
+
*/
|
|
3446
|
+
getImageOnlyPages(doclingDoc) {
|
|
3447
|
+
const picturePages = /* @__PURE__ */ new Set();
|
|
3448
|
+
for (const picture of doclingDoc.pictures) {
|
|
3449
|
+
const pageNo = picture.prov?.[0]?.page_no;
|
|
3450
|
+
if (pageNo != null) {
|
|
3451
|
+
picturePages.add(pageNo);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
if (picturePages.size === 0) return /* @__PURE__ */ new Set();
|
|
3455
|
+
const pageTextLength = /* @__PURE__ */ new Map();
|
|
3456
|
+
for (const text of doclingDoc.texts) {
|
|
3457
|
+
const pageNo = this.getPageNo(text);
|
|
3458
|
+
if (picturePages.has(pageNo)) {
|
|
3459
|
+
pageTextLength.set(
|
|
3460
|
+
pageNo,
|
|
3461
|
+
(pageTextLength.get(pageNo) ?? 0) + text.text.length
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const imageOnlyPages = /* @__PURE__ */ new Set();
|
|
3466
|
+
for (const pageNo of picturePages) {
|
|
3467
|
+
const textLength = pageTextLength.get(pageNo) ?? 0;
|
|
3468
|
+
if (textLength < IMAGE_PAGE_TEXT_THRESHOLD) {
|
|
3469
|
+
imageOnlyPages.add(pageNo);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
return imageOnlyPages;
|
|
3473
|
+
}
|
|
3474
|
+
/**
|
|
3475
|
+
* Get all pages with substantial text content.
|
|
3476
|
+
* Pages with fewer characters than MIN_TEXT_LENGTH (e.g., page numbers, headers only) are excluded.
|
|
3477
|
+
*/
|
|
3478
|
+
getTextPages(doclingDoc) {
|
|
3479
|
+
const pageMap = /* @__PURE__ */ new Map();
|
|
3480
|
+
for (const text of doclingDoc.texts) {
|
|
3481
|
+
const pageNo = this.getPageNo(text);
|
|
3482
|
+
const entry = pageMap.get(pageNo) ?? { textLength: 0, texts: [] };
|
|
3483
|
+
entry.textLength += text.text.length;
|
|
3484
|
+
entry.texts.push(text.text);
|
|
3485
|
+
pageMap.set(pageNo, entry);
|
|
3486
|
+
}
|
|
3487
|
+
return Array.from(pageMap.entries()).filter(([, data]) => data.textLength >= MIN_TEXT_LENGTH).map(([pageNo, data]) => ({
|
|
3488
|
+
pageNo,
|
|
3489
|
+
textLength: data.textLength,
|
|
3490
|
+
texts: data.texts
|
|
3491
|
+
}));
|
|
3492
|
+
}
|
|
3493
|
+
/**
|
|
3494
|
+
* Select sample pages with highest text density.
|
|
3495
|
+
* Prefers pages in the eligible range (not edge-trimmed) and not image-only.
|
|
3496
|
+
* Falls back to all text pages if filtering leaves too few candidates.
|
|
3497
|
+
*/
|
|
3498
|
+
selectSamplePages(textPages, totalPages, imageOnlyPages) {
|
|
3499
|
+
const { frontCutoff, backCutoff } = this.getEligiblePageRange(totalPages);
|
|
3500
|
+
const filtered = totalPages > 0 ? textPages.filter(
|
|
3501
|
+
(p) => p.pageNo > frontCutoff && p.pageNo <= backCutoff && !imageOnlyPages.has(p.pageNo)
|
|
3502
|
+
) : textPages;
|
|
3503
|
+
const candidates = filtered.length > 0 ? filtered : textPages;
|
|
3504
|
+
if (candidates !== filtered) {
|
|
3505
|
+
this.log(
|
|
3506
|
+
"warn",
|
|
3507
|
+
`All text pages were filtered out by edge/image exclusion. Falling back to all ${textPages.length} text pages.`
|
|
3508
|
+
);
|
|
3509
|
+
}
|
|
3510
|
+
const sorted = [...candidates].sort((a, b) => b.textLength - a.textLength);
|
|
3511
|
+
const count = Math.min(MAX_SAMPLE_PAGES, sorted.length);
|
|
3512
|
+
return sorted.slice(0, count);
|
|
3513
|
+
}
|
|
3514
|
+
/**
|
|
3515
|
+
* Evaluate sampled pages using Vision LLM
|
|
3516
|
+
*/
|
|
3517
|
+
async evaluatePages(pages) {
|
|
3518
|
+
const results = [];
|
|
3519
|
+
for (const page of pages) {
|
|
3520
|
+
const result = await this.evaluateSinglePage(page);
|
|
3521
|
+
results.push(result);
|
|
3522
|
+
}
|
|
3523
|
+
return results;
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Evaluate a single page by comparing OCR text against the page image
|
|
3527
|
+
*/
|
|
3528
|
+
async evaluateSinglePage(page) {
|
|
3529
|
+
const imagePath = `pages/page_${page.pageNo - 1}.png`;
|
|
3530
|
+
let imageContent;
|
|
3531
|
+
try {
|
|
3532
|
+
imageContent = this.buildImageContent(imagePath);
|
|
3533
|
+
} catch {
|
|
3534
|
+
this.log(
|
|
3535
|
+
"warn",
|
|
3536
|
+
`Failed to load page image for page ${page.pageNo}, marking as not corrupted`
|
|
3537
|
+
);
|
|
3538
|
+
return {
|
|
3539
|
+
isCorrupted: false,
|
|
3540
|
+
corruptedCharCount: 0,
|
|
3541
|
+
totalKcjCharCount: 0,
|
|
3542
|
+
explanation: `Page image not available for page ${page.pageNo}`
|
|
3543
|
+
};
|
|
3544
|
+
}
|
|
3545
|
+
const ocrText = page.texts.join("\n");
|
|
3546
|
+
const messages = [
|
|
3547
|
+
{
|
|
3548
|
+
role: "user",
|
|
3549
|
+
content: [
|
|
3550
|
+
imageContent,
|
|
3551
|
+
{
|
|
3552
|
+
type: "text",
|
|
3553
|
+
text: this.buildUserPrompt(ocrText)
|
|
3554
|
+
}
|
|
3555
|
+
]
|
|
3556
|
+
}
|
|
3557
|
+
];
|
|
3558
|
+
const { output } = await this.callVisionLLM(
|
|
3559
|
+
HanjaQualityResponseSchema,
|
|
3560
|
+
messages,
|
|
3561
|
+
`page-${page.pageNo}`
|
|
3562
|
+
);
|
|
3563
|
+
this.log(
|
|
3564
|
+
"info",
|
|
3565
|
+
`Page ${page.pageNo}: corrupted=${output.isCorrupted}, ${output.corruptedCharCount}/${output.totalKcjCharCount} KCJ chars corrupted`
|
|
3566
|
+
);
|
|
3567
|
+
return output;
|
|
3568
|
+
}
|
|
3569
|
+
/**
|
|
3570
|
+
* Aggregate individual page results into final assessment
|
|
3571
|
+
*/
|
|
3572
|
+
aggregateResults(totalTextPages, sampledCount, results) {
|
|
3573
|
+
const corruptedCount = results.filter((r) => r.isCorrupted).length;
|
|
3574
|
+
const corruptedRatio = sampledCount > 0 ? corruptedCount / sampledCount : 0;
|
|
3575
|
+
let severity;
|
|
3576
|
+
let needsVlmReparse;
|
|
3577
|
+
if (corruptedRatio >= CORRUPTION_THRESHOLD || corruptedCount >= MIN_SEVERE_CORRUPTED_COUNT) {
|
|
3578
|
+
severity = "severe";
|
|
3579
|
+
needsVlmReparse = true;
|
|
3580
|
+
} else if (corruptedCount > 0) {
|
|
3581
|
+
severity = "minor";
|
|
3582
|
+
needsVlmReparse = false;
|
|
3583
|
+
} else {
|
|
3584
|
+
severity = "none";
|
|
3585
|
+
needsVlmReparse = false;
|
|
3586
|
+
}
|
|
3587
|
+
const reason = corruptedCount === 0 ? "No Hanja character corruption detected" : `${corruptedCount}/${sampledCount} sampled pages have corrupted Hanja characters (ratio: ${corruptedRatio.toFixed(2)})`;
|
|
3588
|
+
return {
|
|
3589
|
+
needsVlmReparse,
|
|
3590
|
+
severity,
|
|
3591
|
+
kcjPageCount: totalTextPages,
|
|
3592
|
+
sampledPageCount: sampledCount,
|
|
3593
|
+
corruptedRatio,
|
|
3594
|
+
reason
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* Get page number from a text item
|
|
3599
|
+
*/
|
|
3600
|
+
getPageNo(text) {
|
|
3601
|
+
return text.prov?.[0]?.page_no ?? 1;
|
|
3602
|
+
}
|
|
3603
|
+
buildSystemPrompt() {
|
|
3604
|
+
return "";
|
|
3605
|
+
}
|
|
3606
|
+
buildUserPrompt(ocrText) {
|
|
3607
|
+
return `You are evaluating the quality of OCR text extraction for Hanja (\u6F22\u5B57) characters used in Korean archaeological reports.
|
|
3608
|
+
|
|
3609
|
+
Look at the page image and find any Hanja (\u6F22\u5B57) characters visible in the original document. Then compare them against the OCR text below.
|
|
3610
|
+
|
|
3611
|
+
## Evaluation Criteria
|
|
3612
|
+
- Find Hanja (\u6F22\u5B57) characters in the page image first
|
|
3613
|
+
- Check if each Hanja character is correctly recognized in the OCR text
|
|
3614
|
+
- A character is "corrupted" if a Hanja character in the image was replaced by Korean hangul, garbled text, symbols, or a completely different character in the OCR text
|
|
3615
|
+
- Common corruption patterns: Hanja replaced by similar-looking hangul (e.g., \u7C98\u571F \u2192 \uADF8\uC73C), wrong character substitution, partial recognition, question marks or boxes
|
|
3616
|
+
- Minor font rendering differences are NOT corruption
|
|
3617
|
+
- If no Hanja characters are visible in the page image, report as not corrupted with 0 total characters
|
|
3618
|
+
|
|
3619
|
+
## OCR Text to Evaluate
|
|
3620
|
+
${ocrText}
|
|
3621
|
+
|
|
3622
|
+
Compare the Hanja characters visible in the page image against the OCR text above and assess whether they were correctly recognized.`;
|
|
3623
|
+
}
|
|
3624
|
+
};
|
|
3625
|
+
|
|
3349
3626
|
// src/validators/base-validator.ts
|
|
3350
3627
|
var BaseValidator = class extends TextLLMComponent {
|
|
3351
3628
|
/**
|
|
@@ -3405,13 +3682,13 @@ var BaseValidator = class extends TextLLMComponent {
|
|
|
3405
3682
|
};
|
|
3406
3683
|
|
|
3407
3684
|
// src/validators/toc-content-validator.ts
|
|
3408
|
-
import { z as
|
|
3409
|
-
var TocContentValidationSchema =
|
|
3410
|
-
isValid:
|
|
3411
|
-
confidence:
|
|
3412
|
-
contentType:
|
|
3413
|
-
extractedTocMarkdown:
|
|
3414
|
-
reason:
|
|
3685
|
+
import { z as z6 } from "zod";
|
|
3686
|
+
var TocContentValidationSchema = z6.object({
|
|
3687
|
+
isValid: z6.boolean().describe("Whether valid main document TOC was found"),
|
|
3688
|
+
confidence: z6.number().min(0).max(1).describe("Confidence score between 0 and 1"),
|
|
3689
|
+
contentType: z6.enum(["pure_toc", "mixed", "resource_only", "invalid"]).describe("Type of content detected"),
|
|
3690
|
+
extractedTocMarkdown: z6.string().nullable().describe("Extracted main TOC markdown when mixed; null otherwise"),
|
|
3691
|
+
reason: z6.string().describe("Brief explanation in English")
|
|
3415
3692
|
});
|
|
3416
3693
|
var TocContentValidator = class extends BaseValidator {
|
|
3417
3694
|
confidenceThreshold;
|
|
@@ -3562,14 +3839,14 @@ ${markdown}`;
|
|
|
3562
3839
|
};
|
|
3563
3840
|
|
|
3564
3841
|
// src/validators/caption-validator.ts
|
|
3565
|
-
import { z as
|
|
3566
|
-
var CaptionValidationItemSchema =
|
|
3567
|
-
index:
|
|
3568
|
-
isValid:
|
|
3569
|
-
reason:
|
|
3842
|
+
import { z as z7 } from "zod";
|
|
3843
|
+
var CaptionValidationItemSchema = z7.object({
|
|
3844
|
+
index: z7.number().int().describe("Index of the caption in the input array"),
|
|
3845
|
+
isValid: z7.boolean().describe("Whether the parsed caption is correct"),
|
|
3846
|
+
reason: z7.string().nullable().describe("Brief explanation if invalid, null if valid")
|
|
3570
3847
|
});
|
|
3571
|
-
var CaptionValidationBatchSchema =
|
|
3572
|
-
results:
|
|
3848
|
+
var CaptionValidationBatchSchema = z7.object({
|
|
3849
|
+
results: z7.array(CaptionValidationItemSchema)
|
|
3573
3850
|
});
|
|
3574
3851
|
var CaptionValidator = class extends BaseValidator {
|
|
3575
3852
|
constructor(logger, model, options, fallbackModel, aggregator) {
|
|
@@ -3777,6 +4054,7 @@ var DocumentProcessor = class {
|
|
|
3777
4054
|
validatorModel;
|
|
3778
4055
|
visionTocExtractorModel;
|
|
3779
4056
|
captionParserModel;
|
|
4057
|
+
hanjaQualitySamplerModel;
|
|
3780
4058
|
textCleanerBatchSize;
|
|
3781
4059
|
captionParserBatchSize;
|
|
3782
4060
|
captionValidatorBatchSize;
|
|
@@ -3803,6 +4081,7 @@ var DocumentProcessor = class {
|
|
|
3803
4081
|
this.validatorModel = options.validatorModel ?? options.fallbackModel;
|
|
3804
4082
|
this.visionTocExtractorModel = options.visionTocExtractorModel ?? options.fallbackModel;
|
|
3805
4083
|
this.captionParserModel = options.captionParserModel ?? options.fallbackModel;
|
|
4084
|
+
this.hanjaQualitySamplerModel = options.hanjaQualitySamplerModel ?? options.fallbackModel;
|
|
3806
4085
|
this.textCleanerBatchSize = options.textCleanerBatchSize;
|
|
3807
4086
|
this.captionParserBatchSize = options.captionParserBatchSize;
|
|
3808
4087
|
this.captionValidatorBatchSize = options.captionValidatorBatchSize;
|
|
@@ -3822,6 +4101,36 @@ var DocumentProcessor = class {
|
|
|
3822
4101
|
throw error;
|
|
3823
4102
|
}
|
|
3824
4103
|
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Assess the quality of KCJ (Hanja) character recognition in the OCR output.
|
|
4106
|
+
*
|
|
4107
|
+
* Samples pages containing KCJ characters and uses Vision LLM to compare
|
|
4108
|
+
* the OCR text against the original page images. Returns an assessment
|
|
4109
|
+
* indicating whether the document should be re-parsed with VLM pipeline.
|
|
4110
|
+
*
|
|
4111
|
+
* @param doclingDoc - Original document extracted from Docling SDK
|
|
4112
|
+
* @param outputPath - Path containing pages subdirectory (pages/page_0.png, etc.)
|
|
4113
|
+
* @returns Assessment result with severity and recommendation
|
|
4114
|
+
*/
|
|
4115
|
+
async assessHanjaQuality(doclingDoc, outputPath) {
|
|
4116
|
+
this.logger.info(
|
|
4117
|
+
"[DocumentProcessor] Starting Hanja quality assessment..."
|
|
4118
|
+
);
|
|
4119
|
+
const sampler = new HanjaQualitySampler(
|
|
4120
|
+
this.logger,
|
|
4121
|
+
this.hanjaQualitySamplerModel,
|
|
4122
|
+
outputPath,
|
|
4123
|
+
this.maxRetries,
|
|
4124
|
+
this.enableFallbackRetry ? this.fallbackModel : void 0,
|
|
4125
|
+
this.usageAggregator,
|
|
4126
|
+
this.abortSignal
|
|
4127
|
+
);
|
|
4128
|
+
const assessment = await sampler.assess(doclingDoc);
|
|
4129
|
+
this.logger.info(
|
|
4130
|
+
`[DocumentProcessor] Hanja assessment: severity=${assessment.severity}, needsVlmReparse=${assessment.needsVlmReparse}`
|
|
4131
|
+
);
|
|
4132
|
+
return assessment;
|
|
4133
|
+
}
|
|
3825
4134
|
/**
|
|
3826
4135
|
* Converts DoclingDocument to ProcessedDocument with token usage tracking.
|
|
3827
4136
|
*
|
|
@@ -4155,8 +4464,10 @@ var DocumentProcessor = class {
|
|
|
4155
4464
|
);
|
|
4156
4465
|
}
|
|
4157
4466
|
const totalPages = Object.keys(doclingDoc.pages).length;
|
|
4467
|
+
const maxTocPageNo = this.extractMaxPageNumber(markdown);
|
|
4468
|
+
const effectiveTotalPages = maxTocPageNo > totalPages ? void 0 : totalPages;
|
|
4158
4469
|
const tocResult = await this.tocExtractor.extract(markdown, {
|
|
4159
|
-
totalPages
|
|
4470
|
+
totalPages: effectiveTotalPages
|
|
4160
4471
|
});
|
|
4161
4472
|
for (const usage of tocResult.usages) {
|
|
4162
4473
|
this.usageAggregator.track(usage);
|
|
@@ -4171,6 +4482,20 @@ var DocumentProcessor = class {
|
|
|
4171
4482
|
);
|
|
4172
4483
|
return tocResult.entries;
|
|
4173
4484
|
}
|
|
4485
|
+
/**
|
|
4486
|
+
* Extract the maximum page number from TOC markdown
|
|
4487
|
+
*
|
|
4488
|
+
* Parses page numbers from dot-leader patterns (e.g., "..... 175")
|
|
4489
|
+
* to detect compiled volume scenarios where TOC page numbers exceed
|
|
4490
|
+
* the sub-document's page count.
|
|
4491
|
+
*/
|
|
4492
|
+
extractMaxPageNumber(markdown) {
|
|
4493
|
+
const matches = [...markdown.matchAll(/\.{2,}\s*(\d+)/g)];
|
|
4494
|
+
if (matches.length === 0) {
|
|
4495
|
+
return 0;
|
|
4496
|
+
}
|
|
4497
|
+
return Math.max(...matches.map((m) => parseInt(m[1], 10)));
|
|
4498
|
+
}
|
|
4174
4499
|
/**
|
|
4175
4500
|
* Process resource captions (for images and tables)
|
|
4176
4501
|
*
|
|
@@ -4438,6 +4763,7 @@ export {
|
|
|
4438
4763
|
CaptionValidator,
|
|
4439
4764
|
ChapterConverter,
|
|
4440
4765
|
DocumentProcessor,
|
|
4766
|
+
HanjaQualitySampler,
|
|
4441
4767
|
PAGE_NUMBER_PATTERN,
|
|
4442
4768
|
PagePattern,
|
|
4443
4769
|
PageRangeParseError,
|