feat(detector): Add per-page OCR routing with pages_needing_ocr field

For mixed PDFs, callers can now see exactly which pages need OCR instead
of re-analyzing the document. Phase 2 scan iterates all pages for Mixed
PDFs (caching sampled results), while TextBased gets empty and
Scanned/ImageBased gets all pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-02-16 10:13:52 -08:00
co-authored by Claude Opus 4.6
parent d8f341bafc
commit c815de9844
5 changed files with 157 additions and 17 deletions
+28 -14
View File
@@ -31,6 +31,8 @@ pub struct PdfProcessResult {
pub page_count: u32,
/// Processing time in milliseconds
pub processing_time_ms: u64,
/// 1-indexed page numbers that need OCR.
pub pages_needing_ocr: Vec<u32>,
}
/// Process a PDF file with smart detection and extraction
@@ -46,29 +48,34 @@ pub fn process_pdf<P: AsRef<Path>>(path: P) -> Result<PdfProcessResult, PdfError
// Step 1: Smart detection (fast, no full load)
let detection = detect_pdf_type(&path)?;
let page_count = detection.page_count;
let pdf_type = detection.pdf_type;
let pages_needing_ocr = detection.pages_needing_ocr;
let result = match detection.pdf_type {
let result = match pdf_type {
PdfType::TextBased => {
// Step 2: Full extraction with position-aware reading order
let items = extract_text_with_positions(&path)?;
let markdown = to_markdown_from_items(items, MarkdownOptions::default());
PdfProcessResult {
pdf_type: PdfType::TextBased,
pdf_type,
text: None, // We now produce markdown directly
markdown: Some(markdown),
page_count: detection.page_count,
page_count,
processing_time_ms: start.elapsed().as_millis() as u64,
pages_needing_ocr,
}
}
PdfType::Scanned | PdfType::ImageBased => {
// Return early - OCR needed
PdfProcessResult {
pdf_type: detection.pdf_type,
pdf_type,
text: None,
markdown: None,
page_count: detection.page_count,
page_count,
processing_time_ms: start.elapsed().as_millis() as u64,
pages_needing_ocr,
}
}
PdfType::Mixed => {
@@ -77,11 +84,12 @@ pub fn process_pdf<P: AsRef<Path>>(path: P) -> Result<PdfProcessResult, PdfError
let markdown = items.map(|i| to_markdown_from_items(i, MarkdownOptions::default()));
PdfProcessResult {
pdf_type: PdfType::Mixed,
pdf_type,
text: None,
markdown,
page_count: detection.page_count,
page_count,
processing_time_ms: start.elapsed().as_millis() as u64,
pages_needing_ocr,
}
}
};
@@ -97,38 +105,44 @@ pub fn process_pdf_mem(buffer: &[u8]) -> Result<PdfProcessResult, PdfError> {
// Step 1: Smart detection (fast, no full load)
let detection = detector::detect_pdf_type_mem(buffer)?;
let page_count = detection.page_count;
let pdf_type = detection.pdf_type;
let pages_needing_ocr = detection.pages_needing_ocr;
let result = match detection.pdf_type {
let result = match pdf_type {
PdfType::TextBased => {
// Step 2: Full extraction with position-aware reading order
let items = extractor::extract_text_with_positions_mem(buffer)?;
let markdown = to_markdown_from_items(items, MarkdownOptions::default());
PdfProcessResult {
pdf_type: PdfType::TextBased,
pdf_type,
text: None,
markdown: Some(markdown),
page_count: detection.page_count,
page_count,
processing_time_ms: start.elapsed().as_millis() as u64,
pages_needing_ocr,
}
}
PdfType::Scanned | PdfType::ImageBased => PdfProcessResult {
pdf_type: detection.pdf_type,
pdf_type,
text: None,
markdown: None,
page_count: detection.page_count,
page_count,
processing_time_ms: start.elapsed().as_millis() as u64,
pages_needing_ocr,
},
PdfType::Mixed => {
let items = extractor::extract_text_with_positions_mem(buffer).ok();
let markdown = items.map(|i| to_markdown_from_items(i, MarkdownOptions::default()));
PdfProcessResult {
pdf_type: PdfType::Mixed,
pdf_type,
text: None,
markdown,
page_count: detection.page_count,
page_count,
processing_time_ms: start.elapsed().as_millis() as u64,
pages_needing_ocr,
}
}
};