v0.3.0 — first-run UX (model auto-resolve + -o/--output) + PDF hardening
Pure-Rust, CPU-only runner for the Baidu Unlimited-OCR model. Install:
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_ocr/main/install.sh | bash
focr pull # fetch the int8 weights once (~3.9 GB)
focr ocr page.png # or: focr ocr page.png -o page.jsonPrebuilt focr binaries for macOS (arm64 + x86_64), Linux (x86_64 + aarch64), and Windows (x86_64). Each asset has a .sha256 sidecar. Weights are unchanged from v0.2.0 (focr pull fetches them).
The first-run-experience release. A clean-machine install report exposed three
papercuts on the path from "curl the installer" to "OCR a page", and all three are
now closed: the installer no longer aborts under gum, a freshly-pulled model is
found with no flags, and focr ocr can write its result straight to a file —
including structured JSON with bounding boxes. The native PDF path picked up two
robustness fixes on top: a decompression-bomb bound and a machine-visible per-page
skip event. Still pure, memory-safe Rust — no Python, no CUDA, no FFI at inference.
Added
focr ocr -o/--output FILE(bd-sreb). Writes the OCR result to a file
instead of stdout; the format follows the extension —.jsonemits structured
JSON, any other extension (e.g..md) emits markdown, and--jsonforces JSON.
The structured JSON carries the renderedmarkdownplus alayoutarray of
{label, boxes}spans, each box[x1, y1, x2, y2]in source-image pixels (a PDF
nests these under a per-pagepagesarray). The same shape is what--json
prints to stdout. New engine entry pointsOcrEngine::recognize_with_layout/
recognize_dynamic_with_layout(and_modelvariants) return the markdown and
the layout parsed from the same decode, so the two can never disagree
(e3c3e68).- Robot per-page skip event for PDFs (
bd-fck1). The resilient PDF document
loop used to drop an undecodable page silently from the machine stream. It now
emits a structuredpageNDJSON event (status=skipped, 1-based page number, a
machine-classifiableerror_kind) in robot mode, so a consumer can tell the
document is missing pages; human mode keeps the stderr warning.pageis an
already-advertised event kind, so the robot schema stays v1
(ba71ff4).
Fixed
- Fresh-install OCR happy path (
bd-3u6x, critical).focr pullinstalls the
model asunlimited-ocr.int8.focrq, but the defaultfocr ocrlookup previously
searched only the bareunlimited-ocr.focrqbasename — so a freshly-pulled model
was invisible without a manual--model, and the clean-machine happy path was
broken. The resolver now also probes the quant-suffixed namesfocr pull
installs (.int8.focrq,.int4.focrq); a pulled model resolves with no flag. An
exact-basename match still wins over a quant variant
(e3c3e68). - Installer aborted instantly under
gum(bd-1km0). The first status line
renderedgum style … "-> …", sogumparsed the leading->as an unknown
flag, printed its usage, and underset -euo pipefailaborted the whole install.
It only triggered withguminstalled AND an interactive TTY, so no
non-interactive CI run ever exercised it. The wholeset -e/arg-parse class is
fixed: everygum styletext arg gets a--flag terminator;check_disk_space
guards adfthat exits non-zero when the default~/.local/binparent does not
exist yet on a fresh account; the checksum pipelines and a failedinstall
(ETXTBSY on reinstall-while-running) are guarded; the lock path is per-user and
$TMPDIR-aware;install.ps1null-guards the top-levelLOCALAPPDATA/
USERPROFILEJoin-Pathso non-Windowspwshreaches the friendly message, not
a stack trace; and aFOCR_INSTALL_BASE_URLoverride lands for mirrors / airgap /
tests. A new true end-to-end installer test (tests/installer_e2e.sh, wired into
scripts/check.sh+ CI withguminstalled) drives the real installer
through thegum/pty path against a fakefile://release into a fresh-account
dir — so this class of bug can never ship silently again
(7f25594,
89c5b21).
Security
- PDF decompression-bomb bound (
bd-2zpu,bd-2yqe).lopdf's
Stream::decompressed_contentmaterializes the full inflated output before any
length check, so a tiny highly-compressed sole-FlateDecodeimage stream (a "zip
bomb") could inflate to gigabytes and OOM the process.decompressed_streamnow
inflates a soleFlateDecode(no PNG/TIFF predictor) itself via the pure-Rust
flate2ZlibDecoder+Read::take(cap + 1)under anexpected_sample_cap
bound (4× the already-MAX_PIXELS-bounded declared sample bytes); a clean inflate
that overruns the cap is the bomb signal and errors, while a non-zlib raw stream
falls back to lopdf's framing-tolerant decoder. A fresh-eyes follow-up
(bd-2yqe) clamped a hostile declared bit-depth that could otherwise saturate the
cap tou64::MAXand reopen the hole.flate2is a direct dependency now
(already in the lock graph vialopdf/png/tiff; pure-Rustminiz_oxide
backend, no FFI)
(ba71ff4,
89c5b21).