Skip to content

v0.3.0 — first-run UX (model auto-resolve + -o/--output) + PDF hardening

Choose a tag to compare

@Dicklesworthstone Dicklesworthstone released this 30 Jun 04:39

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.json

Prebuilt 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 — .json emits structured
    JSON, any other extension (e.g. .md) emits markdown, and --json forces JSON.
    The structured JSON carries the rendered markdown plus a layout array of
    {label, boxes} spans, each box [x1, y1, x2, y2] in source-image pixels (a PDF
    nests these under a per-page pages array). The same shape is what --json
    prints to stdout. New engine entry points OcrEngine::recognize_with_layout /
    recognize_dynamic_with_layout (and _model variants) 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 structured page NDJSON event (status=skipped, 1-based page number, a
    machine-classifiable error_kind) in robot mode, so a consumer can tell the
    document is missing pages; human mode keeps the stderr warning. page is an
    already-advertised event kind, so the robot schema stays v1
    (ba71ff4).

Fixed

  • Fresh-install OCR happy path (bd-3u6x, critical). focr pull installs the
    model as unlimited-ocr.int8.focrq, but the default focr ocr lookup previously
    searched only the bare unlimited-ocr.focrq basename — 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 names focr 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
    rendered gum style … "-> …", so gum parsed the leading -> as an unknown
    flag, printed its usage, and under set -euo pipefail aborted the whole install.
    It only triggered with gum installed AND an interactive TTY, so no
    non-interactive CI run ever exercised it. The whole set -e/arg-parse class is
    fixed: every gum style text arg gets a -- flag terminator; check_disk_space
    guards a df that exits non-zero when the default ~/.local/bin parent does not
    exist yet on a fresh account; the checksum pipelines and a failed install
    (ETXTBSY on reinstall-while-running) are guarded; the lock path is per-user and
    $TMPDIR-aware; install.ps1 null-guards the top-level LOCALAPPDATA /
    USERPROFILE Join-Path so non-Windows pwsh reaches the friendly message, not
    a stack trace; and a FOCR_INSTALL_BASE_URL override lands for mirrors / airgap /
    tests. A new true end-to-end installer test (tests/installer_e2e.sh, wired into
    scripts/check.sh + CI with gum installed) drives the real installer
    through the gum/pty path against a fake file:// 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_content materializes the full inflated output before any
    length check, so a tiny highly-compressed sole-FlateDecode image stream (a "zip
    bomb") could inflate to gigabytes and OOM the process. decompressed_stream now
    inflates a sole FlateDecode (no PNG/TIFF predictor) itself via the pure-Rust
    flate2 ZlibDecoder + Read::take(cap + 1) under an expected_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 to u64::MAX and reopen the hole. flate2 is a direct dependency now
    (already in the lock graph via lopdf/png/tiff; pure-Rust miniz_oxide
    backend, no FFI)
    (ba71ff4,
    89c5b21).