Core Docs

Processing Pipelines

Processing Pipelines



What each job type does, which tools it uses, and how the OCR routing logic works.

Pipeline Overview



PipelineJob TypesPrimary ToolsQueue






ImageCompress, resize, format convert, thumbnailImageMagick, libvipsimages
DocumentPDF preview, text extraction, page renderingGhostscript, pdfimages, pdftotextdocuments
OCRText extraction, table detection, AI escalationPaddleOCR, Surya, Tesseractocr
OpenXMLDOCX/XLSX/PPTX processing, conversionPHPOffice, LibreOfficedocuments
MediaVideo/audio transcode, thumbnail, waveformFFmpegmedia




Image Pipeline



Handles all static image operations. Fast, high-throughput, low memory.

Operations




OperationDescriptionTools






image.thumbnailGenerate thumbnail at specified dimensionslibvips (fast) or ImageMagick
image.compressReduce file size while maintaining visual qualityImageMagick, libvips
image.convertFormat conversion (PNG → WebP, JPEG → AVIF, etc.)ImageMagick
image.resizeResize to specific dimensionslibvips
image.strip_metadataRemove EXIF, GPS, camera dataImageMagick

Processing Flow



1. Download source image from S3
2. Validate: check magic bytes, dimensions, color space
3. Apply operation (via ImageMagick or libvips CLI)
4. Optionally generate multiple sizes in one pass
5. Upload derived image(s) to S3
6. Register outputs in database


Multi-Size Generation



When a user uploads a photo, the app typically needs multiple sizes. Instead of separate jobs, a single image.thumbnail job can generate all sizes in one pass:

{
"job_type": "image.thumbnail",
"params": {
"sizes": [
{ "name": "thumb", "width": 200, "height": 200, "fit": "cover" },
{ "name": "medium", "width": 800, "height": 600, "fit": "inside" },
{ "name": "large", "width": 1920, "height": 1080, "fit": "inside" }
],
"format": "webp",
"quality": 85
}
}


This produces 3 outputs from a single job — much more efficient than 3 separate jobs (one download, 3 resizes, 3 uploads).

Format Strategy




FormatUse CaseQuality Setting






WebPDefault for all web-served images85 (lossy)
AVIFNext-gen format for modern browsers80 (lossy)
JPEG XLArchival / high-quality90 (lossy)
PNGScreenshots, UI elements, transparencyLossless
OriginalPreserved untouched in S3

Originals are never modified or deleted. Derived images are stored alongside the original with deterministic keys.




Document Pipeline



Handles PDF and text-based document operations.

Operations




OperationDescriptionTools






document.pdf_previewRender first N pages as imagesGhostscript → ImageMagick
document.pdf_pagesRender all pages as individual imagesGhostscript → ImageMagick
document.pdf_textExtract text from PDF (non-OCR)pdftotext (poppler-utils)
document.pdf_infoExtract metadata (page count, dimensions, fonts)pdfinfo
document.thumbnailGenerate document thumbnail (first page)Ghostscript → ImageMagick

PDF Preview Flow



1. Download PDF from S3
2. pdfinfo → get page count, dimensions
3. Ghostscript: render pages at 150 DPI → PNG files
4. ImageMagick: convert PNGs to WebP, generate thumbnails
5. Upload page images to S3:
derived/{pool_id}/{object_id}/pdf-preview:v2/page-001.webp
derived/{pool_id}/{object_id}/pdf-preview:v2/page-002.webp
...
6. Upload thumbnail to S3:
derived/{pool_id}/{object_id}/pdf-preview:v2/thumbnail.webp
7. Register outputs


Text Extraction (Non-OCR)



For PDFs with embedded text (not scanned images), pdftotext is fast and accurate. This is the preferred method when available — no OCR needed.

The worker detects whether a PDF has embedded text by running pdftotext on the first page. If the output contains meaningful text (> 50 characters), full extraction proceeds without OCR. If not, the job is re-routed to the OCR pipeline.




OCR Pipeline



The most complex pipeline. Uses a two-pass approach: cheap classical OCR first, AI OCR only when needed.

OCR Engines




EngineTypeStrengthsWhen Used





PaddleOCR (PP-Structure)Classical, layout-awareTable detection, multi-language, fastDefault first pass
SuryaClassical, layout-aware90+ languages, table recognitionAlternative first pass
TesseractClassicalFast, lightweight, good for clean scansFallback for simple documents
AI OCR (via core.ai)LLM-basedComplex tables, handwriting, degraded scansEscalation only

Routing Logic



The key insight: not every document needs AI OCR. A clean, single-column typed letter works perfectly with classical OCR at a fraction of the cost. Only complex documents (tables, multi-column layouts, degraded scans) benefit from AI.

#### First Pass: Classical OCR + Layout Analysis

Every document goes through PaddleOCR PP-Structure (or Surya) first. This produces:

  • Extracted text

  • Layout analysis (text blocks, tables, images)

  • Table detection with bounding boxes

  • Confidence scores per region

#### Routing Decision

From the first-pass output, compute features per page:

def compute_routing_features(paddle_result, page_width, page_height):
tables = paddle_result.get("tables", [])
page_area = page_width * page_height

features = {
"n_tables": len(tables),
"table_area_ratio": sum(t["cell_area_sum"] for t in tables) / page_area if tables else 0,
"max_table_rows": max((len(t.get("rows", [])) for t in tables), default=0),
"max_table_cols": max((len(t.get("cols", [])) for t in tables), default=0),
"n_columns": detect_column_count(paddle_result),
"avg_confidence": paddle_result.get("avg_confidence", 0),
"skew_angle": paddle_result.get("skew_angle", 0),
"junk_token_ratio": compute_junk_ratio(paddle_result),
}
return features

def should_escalate_to_ai(features):
"""Route to AI OCR if any of these conditions are met."""

# Significant table content
if features["n_tables"] >= 1 and features["table_area_ratio"] > 0.15:
if features["max_table_rows"] >= 6 and features["max_table_cols"] >= 4:
return True, "large_table"

# Very large tables (even if area is small)
if features["max_table_rows"] >= 10 or features["max_table_cols"] >= 8:
return True, "complex_table"

# Multi-column layout
if features["n_columns"] >= 2:
return True, "multi_column"

# Poor quality scan with low confidence
if features["skew_angle"] > 3 and features["avg_confidence"] < 0.85:
return True, "low_quality"

# High junk ratio (OCR producing nonsense)
if features["junk_token_ratio"] > 0.20:
return True, "high_junk"

return False, "classical_ok"


#### Routing Decision Flow

Document uploaded


Classical OCR (PaddleOCR PP-Structure)

├── Extract text + layout + tables


Compute routing features

├── Features indicate simple document
│ └── DONE — use classical OCR result
│ (cheap, fast, good enough)

└── Features indicate complex document


AI OCR (via core.ai → vision LLM)

├── Send page images + routing context
├── Receive structured extraction


Merge results

└── DONE — use AI OCR result
(expensive, accurate, handles complexity)


#### Per-Page Routing

Routing is per-page, not per-document. A 50-page lab report might have 48 pages of clean text (classical OCR) and 2 pages with complex tables (AI OCR). Only the 2 complex pages are escalated.

Page 1:  text → classical OCR ✓ (cheap)
Page 2: text → classical OCR ✓
Page 3: table → AI OCR ✓ (expensive, but needed)
Page 4: text → classical OCR ✓
...
Page 50: table → AI OCR ✓


#### Cost Impact


MethodCost per PageWhen




pdftotext (embedded text)~0 creditsPDF has embedded text
Classical OCR (PaddleOCR)LowClean, single-column documents
AI OCR (via core.ai)HighTables, multi-column, degraded scans

Typical split: 70–90% of pages stay classical, 10–30% escalate to AI.

OCR Output Format



Both classical and AI OCR produce the same output structure:

{
"pages": [
{
"page_number": 1,
"method": "paddleocr",
"confidence": 0.94,
"text": "Full extracted text...",
"blocks": [
{ "type": "text", "bbox": [x, y, w, h], "content": "..." },
{ "type": "table", "bbox": [x, y, w, h], "rows": [...], "cols": [...] }
],
"language": "fra"
}
],
"routing": {
"total_pages": 50,
"classical_pages": 47,
"ai_pages": 3,
"escalation_reasons": ["large_table", "multi_column"]
}
}


The output is stored as a JSON file in S3: derived/{pool_id}/{object_id}/ocr-standard:v2/result.json.




OpenXML Pipeline



Handles Microsoft Office formats. Uses native PHP libraries for modern formats, LibreOffice for legacy conversion.

Format Support




FormatExtensionMethodLibrary







Word (modern).docxNative OpenXMLPhpWord
Word (legacy).docLibreOffice → .docx → processLibreOffice headless
Excel (modern).xlsxNative OpenXMLPHPSpreadsheet
Excel (legacy).xlsLibreOffice → .xlsx → processLibreOffice headless
PowerPoint (modern).pptxNative OpenXMLPHPPresentation
PowerPoint (legacy).pptLibreOffice → .pptx → processLibreOffice headless

Operations




OperationDescriptionTools







openxml.extract_textExtract all text contentPhpWord / PHPSpreadsheet / PHPPresentation
openxml.to_htmlConvert to HTML for web displayPhpWord → HTML
openxml.to_pdfConvert to PDFLibreOffice headless (best fidelity)
openxml.extract_dataExtract structured data (spreadsheet rows, form fields)PHPSpreadsheet
openxml.thumbnailGenerate first-page thumbnailLibreOffice → PDF → Ghostscript
openxml.convert_legacyConvert .doc → .docx, .xls → .xlsx, .ppt → .pptxLibreOffice headless

Legacy Conversion Flow



1. User uploads legacy file (.doc, .xls, .ppt)
2. Job created: openxml.convert_legacy
3. Worker:
a. Download source from S3
b. Validate file signature (not just extension)
c. Check for macros — reject .docm/.xlsm/.pptm
d. Run LibreOffice headless:
soffice --headless --convert-to docx --outdir /work/jobs/123/ input.doc
e. Validate output
f. Upload converted file to S3
g. Register output
4. Subsequent operations use the converted modern file


PDF Export (High Fidelity)



LibreOffice headless produces the best PDF conversion quality for Office documents:

soffice --headless --convert-to pdf:writer_pdf_Export \
--outdir /work/jobs/123/ \
/work/jobs/123/input.docx


This is used for:
  • Generating printable versions

  • Creating PDF thumbnails

  • Archival conversion

Security



  • Macro rejection: .docm, .xlsm, .pptm files are rejected at job creation. No exceptions.

  • Sandboxed execution: LibreOffice runs in a restricted environment with no network access and a clean user profile per invocation.

  • File validation: Magic bytes are checked. A .docx file that isn't a valid ZIP archive is rejected.

  • Temp isolation: Each conversion gets a fresh temp directory. LibreOffice profile data is not shared between jobs.




Media Pipeline



Handles audio and video processing via FFmpeg. The most resource-intensive pipeline.

Operations




OperationDescriptionTools







media.transcodeConvert between formats (e.g., MOV → MP4)FFmpeg
media.compressReduce file size (bitrate, resolution)FFmpeg
media.thumbnailExtract frame at timestamp as imageFFmpeg → ImageMagick
media.waveformGenerate audio waveform visualizationFFmpeg → custom renderer
media.extract_audioExtract audio track from videoFFmpeg
media.trimTrim video/audio to time rangeFFmpeg

Transcode Profiles




ProfileCodecUse Case






video-h264-webH.264 + AAC, CRF 23Web playback (universal)
video-h265-archiveH.265 + AAC, CRF 28Storage-efficient archive
video-webmVP9 + OpusModern browsers, smaller size
audio-mp3MP3, 192kbpsUniversal audio playback
audio-aacAAC, 256kbpsHigh-quality audio

Processing Flow



1. Download source from S3
2. ffprobe → get duration, resolution, codecs, bitrate
3. Estimate processing time (based on duration × complexity factor)
4. If estimated time > 30 minutes: route to spot/burst worker
5. Run FFmpeg with progress reporting
6. Generate thumbnail from first frame
7. Upload transcoded file + thumbnail to S3
8. Register outputs


Progress Reporting



FFmpeg reports progress to stderr. The worker parses this and updates the job status periodically:

FFmpeg stderr: frame= 1234 fps=30 q=28.0 size=   45678kB time=00:00:41.23 ...
▲ ▲
│ │
parsed by worker current position
│ │
▼ ▼
UPDATE processing_jobs SET progress_json =
{"frame": 1234, "time_s": 41.23, "percent": 34.4}


Apps poll the job status endpoint and display a progress bar.




Output Key Structure



All derived files follow a deterministic key structure:

derived/{pool_id}/{object_id}/{profile_key}:{profile_version}/{output_file}


Examples:
derived/pool-abc/42/pdf-preview:v2/page-001.webp
derived/pool-abc/42/pdf-preview:v2/thumbnail.webp
derived/pool-abc/42/ocr-standard:v2/result.json
derived/pool-abc/42/image-thumbnail:v3/thumb.webp
derived/pool-abc/42/image-thumbnail:v3/medium.webp
derived/pool-abc/42/video-h264-web:v1/output.mp4


Benefits:
  • Idempotent: re-running the same job produces the same keys (overwrites harmlessly)

  • Versioned: changing a profile creates a new version — old outputs coexist

  • Queryable: all derived files for an object are under a single prefix

  • Cleanable: deleting an object's derived files is a prefix delete

Related