@heripo/pdf-parser 0.1.14 → 0.1.16
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 +138 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +137 -1
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.d.cts
CHANGED
|
@@ -41,6 +41,8 @@ type PDFConvertOptions = Omit<ConversionOptions, 'to_formats' | 'image_export_mo
|
|
|
41
41
|
chunkSize?: number;
|
|
42
42
|
/** Max retry attempts per failed chunk (default: CHUNKED_CONVERSION.DEFAULT_MAX_RETRIES) */
|
|
43
43
|
chunkMaxRetries?: number;
|
|
44
|
+
/** LLM model for document type validation (opt-in: skipped when not set) */
|
|
45
|
+
documentValidationModel?: LanguageModel;
|
|
44
46
|
};
|
|
45
47
|
/** Result of strategy-based conversion */
|
|
46
48
|
interface ConvertWithStrategyResult {
|
|
@@ -179,6 +181,17 @@ declare class ImagePdfFallbackError extends Error {
|
|
|
179
181
|
constructor(originalError: Error, fallbackError: Error);
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Error thrown when the uploaded PDF does not appear to be
|
|
186
|
+
* a Korean archaeological investigation report.
|
|
187
|
+
*/
|
|
188
|
+
declare class InvalidDocumentTypeError extends Error {
|
|
189
|
+
readonly reason: string;
|
|
190
|
+
readonly name = "InvalidDocumentTypeError";
|
|
191
|
+
readonly code = "INVALID_DOCUMENT_TYPE";
|
|
192
|
+
constructor(reason: string);
|
|
193
|
+
}
|
|
194
|
+
|
|
182
195
|
/**
|
|
183
196
|
* Intermediate format produced by VLM page-by-page processing.
|
|
184
197
|
* Intentionally kept simple so VLM prompts stay short and accurate.
|
|
@@ -287,4 +300,4 @@ declare class VlmResponseValidator {
|
|
|
287
300
|
private static detectRepetitivePattern;
|
|
288
301
|
}
|
|
289
302
|
|
|
290
|
-
export { type ConversionCompleteCallback, type ConvertWithStrategyResult, ImagePdfFallbackError, type PDFConvertOptions, PDFParser, type VlmPageQuality, type VlmQualityIssue, type VlmQualityIssueType, VlmResponseValidator, type VlmValidationResult };
|
|
303
|
+
export { type ConversionCompleteCallback, type ConvertWithStrategyResult, ImagePdfFallbackError, InvalidDocumentTypeError, type PDFConvertOptions, PDFParser, type VlmPageQuality, type VlmQualityIssue, type VlmQualityIssueType, VlmResponseValidator, type VlmValidationResult };
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ type PDFConvertOptions = Omit<ConversionOptions, 'to_formats' | 'image_export_mo
|
|
|
41
41
|
chunkSize?: number;
|
|
42
42
|
/** Max retry attempts per failed chunk (default: CHUNKED_CONVERSION.DEFAULT_MAX_RETRIES) */
|
|
43
43
|
chunkMaxRetries?: number;
|
|
44
|
+
/** LLM model for document type validation (opt-in: skipped when not set) */
|
|
45
|
+
documentValidationModel?: LanguageModel;
|
|
44
46
|
};
|
|
45
47
|
/** Result of strategy-based conversion */
|
|
46
48
|
interface ConvertWithStrategyResult {
|
|
@@ -179,6 +181,17 @@ declare class ImagePdfFallbackError extends Error {
|
|
|
179
181
|
constructor(originalError: Error, fallbackError: Error);
|
|
180
182
|
}
|
|
181
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Error thrown when the uploaded PDF does not appear to be
|
|
186
|
+
* a Korean archaeological investigation report.
|
|
187
|
+
*/
|
|
188
|
+
declare class InvalidDocumentTypeError extends Error {
|
|
189
|
+
readonly reason: string;
|
|
190
|
+
readonly name = "InvalidDocumentTypeError";
|
|
191
|
+
readonly code = "INVALID_DOCUMENT_TYPE";
|
|
192
|
+
constructor(reason: string);
|
|
193
|
+
}
|
|
194
|
+
|
|
182
195
|
/**
|
|
183
196
|
* Intermediate format produced by VLM page-by-page processing.
|
|
184
197
|
* Intentionally kept simple so VLM prompts stay short and accurate.
|
|
@@ -287,4 +300,4 @@ declare class VlmResponseValidator {
|
|
|
287
300
|
private static detectRepetitivePattern;
|
|
288
301
|
}
|
|
289
302
|
|
|
290
|
-
export { type ConversionCompleteCallback, type ConvertWithStrategyResult, ImagePdfFallbackError, type PDFConvertOptions, PDFParser, type VlmPageQuality, type VlmQualityIssue, type VlmQualityIssueType, VlmResponseValidator, type VlmValidationResult };
|
|
303
|
+
export { type ConversionCompleteCallback, type ConvertWithStrategyResult, ImagePdfFallbackError, InvalidDocumentTypeError, type PDFConvertOptions, PDFParser, type VlmPageQuality, type VlmQualityIssue, type VlmQualityIssueType, VlmResponseValidator, type VlmValidationResult };
|
package/dist/index.js
CHANGED
|
@@ -1484,6 +1484,32 @@ var PdfTextExtractor = class {
|
|
|
1484
1484
|
}
|
|
1485
1485
|
return result.stdout;
|
|
1486
1486
|
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Extract text from a range of PDF pages using a single pdftotext invocation.
|
|
1489
|
+
* Returns empty string on failure (logged as warning).
|
|
1490
|
+
*
|
|
1491
|
+
* @param pdfPath - Absolute path to the source PDF file
|
|
1492
|
+
* @param firstPage - First page number (1-based)
|
|
1493
|
+
* @param lastPage - Last page number (1-based, inclusive)
|
|
1494
|
+
*/
|
|
1495
|
+
async extractPageRange(pdfPath, firstPage, lastPage) {
|
|
1496
|
+
const result = await spawnAsync("pdftotext", [
|
|
1497
|
+
"-f",
|
|
1498
|
+
firstPage.toString(),
|
|
1499
|
+
"-l",
|
|
1500
|
+
lastPage.toString(),
|
|
1501
|
+
"-layout",
|
|
1502
|
+
pdfPath,
|
|
1503
|
+
"-"
|
|
1504
|
+
]);
|
|
1505
|
+
if (result.code !== 0) {
|
|
1506
|
+
this.logger.warn(
|
|
1507
|
+
`[PdfTextExtractor] pdftotext failed for pages ${firstPage}-${lastPage}: ${result.stderr || "Unknown error"}`
|
|
1508
|
+
);
|
|
1509
|
+
return "";
|
|
1510
|
+
}
|
|
1511
|
+
return result.stdout;
|
|
1512
|
+
}
|
|
1487
1513
|
/**
|
|
1488
1514
|
* Extract text from a single PDF page using pdftotext.
|
|
1489
1515
|
* Returns empty string on failure (logged as warning).
|
|
@@ -2376,6 +2402,94 @@ async function getTaskFailureDetails(task, logger, logPrefix) {
|
|
|
2376
2402
|
return "unable to retrieve error details";
|
|
2377
2403
|
}
|
|
2378
2404
|
|
|
2405
|
+
// src/validators/document-type-validator.ts
|
|
2406
|
+
import { z as z3 } from "zod";
|
|
2407
|
+
|
|
2408
|
+
// src/errors/invalid-document-type-error.ts
|
|
2409
|
+
var InvalidDocumentTypeError = class extends Error {
|
|
2410
|
+
constructor(reason) {
|
|
2411
|
+
super(
|
|
2412
|
+
`The uploaded PDF does not appear to be a Korean archaeological investigation report. Reason: ${reason}`
|
|
2413
|
+
);
|
|
2414
|
+
this.reason = reason;
|
|
2415
|
+
}
|
|
2416
|
+
name = "InvalidDocumentTypeError";
|
|
2417
|
+
code = "INVALID_DOCUMENT_TYPE";
|
|
2418
|
+
};
|
|
2419
|
+
|
|
2420
|
+
// src/validators/document-type-validator.ts
|
|
2421
|
+
var SYSTEM_PROMPT = `You are given text extracted from the first and last pages of a PDF document.
|
|
2422
|
+
Determine if this document is an archaeological investigation report from any country.
|
|
2423
|
+
|
|
2424
|
+
Valid types include (in any language):
|
|
2425
|
+
- Excavation report (\uBC1C\uAD74\uC870\uC0AC\uBCF4\uACE0\uC11C)
|
|
2426
|
+
- Trial excavation report (\uC2DC\uAD74\uC870\uC0AC\uBCF4\uACE0\uC11C)
|
|
2427
|
+
- Surface survey report (\uC9C0\uD45C\uC870\uC0AC\uBCF4\uACE0\uC11C)
|
|
2428
|
+
- Detailed excavation report (\uC815\uBC00\uBC1C\uAD74\uC870\uC0AC\uBCF4\uACE0\uC11C)
|
|
2429
|
+
- Underwater excavation report (\uC218\uC911\uBC1C\uAD74\uC870\uC0AC\uBCF4\uACE0\uC11C)
|
|
2430
|
+
- Salvage excavation report
|
|
2431
|
+
- Archaeological assessment report
|
|
2432
|
+
- Any other archaeological fieldwork investigation report
|
|
2433
|
+
|
|
2434
|
+
NOT valid (these are NOT archaeological investigation reports):
|
|
2435
|
+
- Repair/restoration reports (\uC218\uB9AC\uBCF4\uACE0\uC11C)
|
|
2436
|
+
- Simple measurement reports (\uB2E8\uC21C \uC2E4\uCE21 \uBCF4\uACE0\uC11C)
|
|
2437
|
+
- Architectural investigation reports (\uAC74\uCD95\uC870\uC0AC\uBCF4\uACE0\uC11C)
|
|
2438
|
+
- Academic research reports (\uD559\uC220\uC870\uC0AC\uBCF4\uACE0\uC11C)
|
|
2439
|
+
- Environmental impact assessments (\uD658\uACBD\uC601\uD5A5\uD3C9\uAC00)
|
|
2440
|
+
- General academic papers or textbooks about archaeology
|
|
2441
|
+
- Conservation/preservation reports
|
|
2442
|
+
- Museum catalogs or exhibition guides`;
|
|
2443
|
+
var documentTypeSchema = z3.object({
|
|
2444
|
+
isValid: z3.boolean().describe("Whether this is an archaeological investigation report"),
|
|
2445
|
+
reason: z3.string().describe("Brief reason for the decision")
|
|
2446
|
+
});
|
|
2447
|
+
var DocumentTypeValidator = class {
|
|
2448
|
+
textExtractor;
|
|
2449
|
+
constructor(textExtractor) {
|
|
2450
|
+
this.textExtractor = textExtractor;
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Validate that the PDF at the given path is an archaeological investigation report.
|
|
2454
|
+
*
|
|
2455
|
+
* @throws {InvalidDocumentTypeError} if the document is not a valid report type
|
|
2456
|
+
*/
|
|
2457
|
+
async validate(pdfPath, model, options) {
|
|
2458
|
+
const totalPages = await this.textExtractor.getPageCount(pdfPath);
|
|
2459
|
+
if (totalPages === 0) return;
|
|
2460
|
+
const frontText = await this.textExtractor.extractPageRange(
|
|
2461
|
+
pdfPath,
|
|
2462
|
+
1,
|
|
2463
|
+
Math.min(10, totalPages)
|
|
2464
|
+
);
|
|
2465
|
+
let backText = "";
|
|
2466
|
+
if (totalPages > 20) {
|
|
2467
|
+
backText = await this.textExtractor.extractPageRange(
|
|
2468
|
+
pdfPath,
|
|
2469
|
+
Math.max(1, totalPages - 9),
|
|
2470
|
+
totalPages
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
const combinedText = (frontText + "\n" + backText).trim();
|
|
2474
|
+
if (combinedText.length === 0) return;
|
|
2475
|
+
const result = await LLMCaller.call({
|
|
2476
|
+
schema: documentTypeSchema,
|
|
2477
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
2478
|
+
userPrompt: `--- Document text (first and last pages) ---
|
|
2479
|
+
${combinedText}`,
|
|
2480
|
+
primaryModel: model,
|
|
2481
|
+
maxRetries: 2,
|
|
2482
|
+
temperature: 0,
|
|
2483
|
+
abortSignal: options?.abortSignal,
|
|
2484
|
+
component: "DocumentTypeValidator",
|
|
2485
|
+
phase: "validation"
|
|
2486
|
+
});
|
|
2487
|
+
if (!result.output.isValid) {
|
|
2488
|
+
throw new InvalidDocumentTypeError(result.output.reason);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
|
|
2379
2493
|
// src/core/chunked-pdf-converter.ts
|
|
2380
2494
|
import {
|
|
2381
2495
|
copyFileSync,
|
|
@@ -3003,8 +3117,27 @@ var PDFConverter = class {
|
|
|
3003
3117
|
this.enableImagePdfFallback = enableImagePdfFallback;
|
|
3004
3118
|
this.timeout = timeout;
|
|
3005
3119
|
}
|
|
3120
|
+
documentTypeValidated = false;
|
|
3121
|
+
/**
|
|
3122
|
+
* Validate that the PDF is a Korean archaeological investigation report.
|
|
3123
|
+
* Skipped when no documentValidationModel is configured or for non-local URLs.
|
|
3124
|
+
* Only runs once per converter instance (flag prevents duplicate checks on recursive calls).
|
|
3125
|
+
*/
|
|
3126
|
+
async validateDocumentType(url, options, abortSignal) {
|
|
3127
|
+
if (this.documentTypeValidated) return;
|
|
3128
|
+
this.documentTypeValidated = true;
|
|
3129
|
+
if (!options.documentValidationModel) return;
|
|
3130
|
+
const pdfPath = url.startsWith("file://") ? url.slice(7) : null;
|
|
3131
|
+
if (!pdfPath) return;
|
|
3132
|
+
const textExtractor = new PdfTextExtractor(this.logger);
|
|
3133
|
+
const validator = new DocumentTypeValidator(textExtractor);
|
|
3134
|
+
await validator.validate(pdfPath, options.documentValidationModel, {
|
|
3135
|
+
abortSignal
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3006
3138
|
async convert(url, reportId, onComplete, cleanupAfterCallback, options, abortSignal) {
|
|
3007
3139
|
this.logger.info("[PDFConverter] Converting:", url);
|
|
3140
|
+
await this.validateDocumentType(url, options, abortSignal);
|
|
3008
3141
|
if (options.chunkedConversion && url.startsWith("file://")) {
|
|
3009
3142
|
const chunked = new ChunkedPDFConverter(
|
|
3010
3143
|
this.logger,
|
|
@@ -3059,6 +3192,7 @@ var PDFConverter = class {
|
|
|
3059
3192
|
const aggregator = options.aggregator ?? new LLMTokenUsageAggregator();
|
|
3060
3193
|
const trackedOptions = { ...options, aggregator };
|
|
3061
3194
|
const pdfPath = url.startsWith("file://") ? url.slice(7) : null;
|
|
3195
|
+
await this.validateDocumentType(url, trackedOptions, abortSignal);
|
|
3062
3196
|
const strategy = await this.determineStrategy(
|
|
3063
3197
|
pdfPath,
|
|
3064
3198
|
reportId,
|
|
@@ -3385,7 +3519,8 @@ var PDFConverter = class {
|
|
|
3385
3519
|
"onTokenUsage",
|
|
3386
3520
|
"chunkedConversion",
|
|
3387
3521
|
"chunkSize",
|
|
3388
|
-
"chunkMaxRetries"
|
|
3522
|
+
"chunkMaxRetries",
|
|
3523
|
+
"documentValidationModel"
|
|
3389
3524
|
]),
|
|
3390
3525
|
to_formats: ["json", "html"],
|
|
3391
3526
|
image_export_mode: "embedded",
|
|
@@ -4086,6 +4221,7 @@ var VlmResponseValidator = class {
|
|
|
4086
4221
|
};
|
|
4087
4222
|
export {
|
|
4088
4223
|
ImagePdfFallbackError,
|
|
4224
|
+
InvalidDocumentTypeError,
|
|
4089
4225
|
PDFParser,
|
|
4090
4226
|
VlmResponseValidator
|
|
4091
4227
|
};
|