Tesseract OCR in a pipeline: ordering, quarantine, empty output
Adding tesseract to an ingest pipeline turned out to be less about the CLI and more about ordering, the img caption convention, quarantine, and empty output.
TL;DR
A media ingest job stalled with media-blocked status after tesseract went unregistered, previously blocking every job including audio-only ones. The fix narrows preflight so OCR is only mandatory for image-only jobs, splitting failures into plan-blocking versus quarantine-worthy problems. Empty OCR output counts as success with evidence recorded, while infrastructure timeouts retry and corrupt content gets quarantined for healing.
An image-only ingest job refused to plan the other evening. The plan command printed exactly one status: media-blocked. My first guess was the boring one: something missing from media.config.json. Correct, tesseract was not registered. What annoyed me was the blast radius, though. In the first version of the config check, one broken media component blocked every job. An audio transcription job that needs no OCR at all stopped dead too.
Looking at how jobs actually reference files, I decided that was the wrong design. OCR is image-only tooling. Audio and video jobs should not trip over a broken OCR entry. So this commit narrows the preflight: tesseract becomes mandatory only when the job's referenced set is purely images. Mixed or audio/video jobs keep planning, and the per-source preflight in the media subcommands still guards the gate.
A missing tool is not the apocalypse
The principle I settled on: tool failures come in two classes. The first class makes the plan untrustworthy, like a required tool missing or a binary whose hash does not match. That class deserves a fast fail at planning, so the operator sees it before anything runs. The second class holds a single file hostage, like one image whose meta is corrupt. That one should not sink the whole job: quarantine it, heal it later.
In the kernel this split lives in one small parameter, need_ocr. If the job is image-only and tesseract is broken, plan rejects with media-blocked. If a job has no images at all, even a corrupt tesseract entry gets skipped, because nothing would consume it.
OCR first, caption later
For the image path the order is rigid: OCR first, caption later. The kernel runs tesseract with the source file and stdout as the output base, so the recognized text arrives on standard output [2]. With no extra flags, tesseract assumes English, page segmentation mode 3, plain text out [4]. From Python it is invoked with capture_output and a 120 second timeout [3].
The output lands verbatim in ocr.txt, next to a meta.json recording kind image, the OCR character count, the OCR sha256, and the tesseract version. Only then does captioning run: one caption per image with the fixed frame id img. Video has many frames and can defer caption budget; an image is one whole surface, so it gets exactly one caption, no deferral.
If the meta is corrupt or missing, the kernel does not crash. The row enters the media-quarantine path, reusing the heal convention the video branch already had. On heal, OCR runs again and existing captions are carried along.
Empty output is data, not drama
The decision people question most: if OCR comes back empty, is that success or failure? My take: success, with the evidence on record. A blank page is a real-world outcome. The official documentation notes that oversized borders, or a bare text area without any border, can produce an empty page [5]. The pipeline has no business believing it is smarter than its tool. Empty text with ocr_chars at zero still gets written, and the consumer above it decides whether that text is useful.
What stays strict is the infrastructure-versus-content split. Timeout, for example: the child process is killed first, then TimeoutExpired is raised [3]. That is infrastructure, exit 5 with no event, attempts unchanged, just retry. Corrupt meta is different: that is content, and content goes to quarantine.
On batch performance: by default tesseract 4 and above can use up to 4 CPU threads per page [6]. Fine for one image. For a long queue, one single-threaded process per core behaves much better: kill internal multithreading with OMP_THREAD_LIMIT=1 [6], and for many files run one process per core in parallel [6]. One more thing that is easy to forget: OCR needs decent input, around 300 DPI at minimum [5]. All of this holds on tesseract 5, stable since 5.0.0 in late 2021 [1], so there is nothing to wait for.
The lesson I took home: adding a media type is never about adding one more branch. The real work is the failure contract. Which failures block planning up front, and which get quarantined and healed slowly. Once that contract is visible, the rest of the code is a formality.