franken_ocr

Dicklesworthstone活跃维护

纯 Rust 实现的 CPU 端 OCR 引擎,跑百度 Unlimited-OCR 这个 3B MoE 视觉语言模型,自带 int8 定制算子,不依赖 ML 框架、Python 或 GPU。

Pure-Rust, CPU-only OCR engine for Baidu Unlimited-OCR (a DeepSeek-OCR-derived 3B MoE VLM). Five-model zoo, custom int8 kernels, no ML framework, no Python, no GPU.

v0.10.0发布于 2026-11-09 13:15 UTC347star +2 / 7 天40fork同步于 2026-11-10 23:59 UTC

怎么上手

由 README 提炼,执行前请确认
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_ocr/main/install.sh | bash
focr pull
硬件要求按 README 说明

仅需 CPU,无需 GPU 或 CUDA;支持 macOS Apple Silicon/Intel、Linux x86-64/ARM64、Windows x86-64/ARM64;Linux 需 glibc 2.17 以上。README 未提及内存或显存要求。

纯 CPU 可运行支持 Apple Silicon需要 CUDA
下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

由 README 自动翻译提炼

纯 Rust 实现的 CPU 版 OCR 引擎,无需 Python、CUDA 或 GPU,就能运行 Baidu Unlimited-OCR 等视觉语言模型

纯 Rust 实现的 CPU 端 OCR 引擎,跑百度 Unlimited-OCR 这个 3B MoE 视觉语言模型,自带 int8 定制算子,不依赖 ML 框架、Python 或 GPU。

核心能力

  • 用自定义 int8 内核运行五个已移植的视觉语言模型
  • 单个二进制文件即可离线推理,无需 ML 框架和 FFI
  • 内置 PDF 光栅化,支持页面旋转、跨页解析和图表提取
  • 提供可嵌入的 Rust API,支持同步阻塞调用和批量加载
  • 运行时按 CPU 指令集自动分派 AVX-512-VNNI、AVX2 等内核

容易踩的坑

安装器只装二进制,模型权重需另外用 focr pull 下载后才能离线推理;从源码构建需 nightly 工具链且依赖同级仓库目录布局;Linux 二进制要求 glibc 2.17 以上。

内容由模型根据 README 生成,可能不完整或不准确,以官方文档为准。看原文 README

最近发版

5 个版本v0.10.0
  1. v0.10.0最新正式版2026-11-09 · 2026-11-09 13:15 UTC

    v0.10.0 is a feature release. It ships the same six-platform raw-executable matrix as v0.9.x (each asset with a SHA256 sidecar) and still pins the hash-verified v0.7.0 Unlimited-OCR model artifact. Install with the one-liner; focr pull fetches weights.

    Changed

    • The default thread budget on an Apple-silicon Mac is now its fastest core tier (bead bd-zex4). It was every physical core, so the slower tier set the pace of each fork-join section while the fast cores spun. The budget now comes from hw.perflevel0.physicalcpu. Intel Macs and iOS are unchanged, and --threads / FOCR_THREADS still override it.
    • --temperature above 0 is refused before a model loads (bead bd-d630). Every model decodes greedily, so a temperature > 0 request used to load the weights and then exit 1; it now fails fast with exit 2 and says why. --temperature 0 is accepted as before.
    • The runaway guard is on by default for a v1 page, and keeps the text it stops (bead bd-gftg). A v1 page that never ends used to decode to the 32,768-token limit and return mostly repetition with exit 0. Now, past 8,192 tokens, the guard watches for sustained repetition, writes the text decoded so far, reports finish_reason: runaway and exits 5 (ocr-batch marks the image failed and continues). FOCR_RUNAWAY_GUARD=0 restores decode-to-limit. The resident daemon protocol moves to 8.
    • Token-level OCR output can differ between x86_64 and aarch64 for the same input (bead bd-q5jo). The int8 GEMM kernels match the scalar oracle bit-for-bit; the difference comes from non-GEMM floating-point paths. x86_64 single-page output is unchanged from 0.9.1.

    Added

    • focr doctor now checks installed models: each model's tokenizer sidecar and whether each installed model actually loads, without decoding (beads bd-uocr2-epic-cgux.37).
    • focr ocr --check: a preflight that runs a run's argument, input, model-header, tokenizer and output-directory checks and then stops (bead bd-m76g).
    • GOT-OCR2 single-region reads: --box x1,y1,x2,y2 (or a point) and --color red|green|blue, composable with --format (bead bd-1sj1).
    • Robot stage events (load / forward, with elapsed and budget), and the resident daemon now serves --task music and --model unlimited-ocr-2 runs with full event and exit-code parity (beads bd-fuh2, bd-u77k).
    • Unlimited-OCR-2: optional capped-page re-read (FOCR_REREAD_CAPPED_PAGES=1, bead bd-yrr1) and decode-phase timing rows (bead bd-uocr2-epic-cgux.38).

    Build

    • Built by the dsr local release pipeline (not GitHub Actions) from source commit dcae29126274388d2ad7dfc1730be8d0bd56eb29, with the lockfile-pinned sibling closure: asupersync 7a0f2f55, frankentorch 4b23eb8a, frankensqlite 2f401ee7.
    • Six raw executables — macOS arm64 and x86_64, Linux x86_64 and aarch64, Windows x86_64 and aarch64 — each with a SHA256 sidecar. The Linux binaries target the documented glibc 2.17 floor.

    Verification

    • All six platform binaries built and collected, with the expected machine type for each target.
    • Windows PE parity against v0.9.1: both the x86_64 and aarch64 .exe match on machine and subsystem, imported-DLL list, and C-runtime model (the dynamic vcruntime140.dll + api-ms-win-crt-* set), so no new runtime redistributable is required.
    • The Linux binaries' highest referenced glibc symbol version is 2.17.
    • Each asset's SHA256 sidecar matches its binary.
    • crates.io publishability was confirmed with cargo publish --dry-run --locked.

    Full history: see CHANGELOG.md §0.10.0.

  2. models-unlimited-ocr-2-v1Unlimited-OCR-2 int8 weights (models-unlimited-ocr-2-v1)2026-11-02 · 2026-11-02 17:38 UTC
    查看更新说明

    int8 .focrq weights for franken_ocr's second model, Baidu Unlimited-OCR-2 (MIT). Recipe unlimited-ocr-2-ffn-int8-attn-bf16-lmhead-bf16-v1, converted from HF baidu/Unlimited-OCR-2 at commit 5632329a.

    The reassembled unlimited-ocr-2.v0.1.0.int8.focrq is 4,173,206,653 B, sha256 54b8a1ecaf0c1311eab5bb16cf224bca67772056e1d713d5538b5486035b4398. GitHub caps an asset at 2 GiB, so it ships as three parts. focr pull unlimited-ocr-2 reassembles them and checks the SHA-256 of each part and of the whole file:

    • unlimited-ocr-2.v0.1.0.int8.focrq.part00: 1,391,068,885 B, sha256 9e940a5891b6abcbfc87594c3dc943ed80cd835083d332758f62763606e0b227
    • unlimited-ocr-2.v0.1.0.int8.focrq.part01: 1,391,068,885 B, sha256 111efd9e7f7b6fbdf1efbf7101438bf79c4222aacaef18f1a9c9a2bf92ba3cd6
    • unlimited-ocr-2.v0.1.0.int8.focrq.part02: 1,391,068,883 B, sha256 34bd2eba13ce4afe75a90fd21710149476d0cc0f89c285449cd22b4466aeca02
    • tokenizer.json: 9,980,278 B, sha256 c55c165e32c5a67830362a53e1f548c8930dcc8469efa30ff3252a982799c589. It is the v2 tokenizer, byte-identical to the one at baidu/Unlimited-OCR-2 @5632329a, and not v1's.

    To reassemble by hand: cat unlimited-ocr-2.v0.1.0.int8.focrq.part0{0,1,2} > unlimited-ocr-2.v0.1.0.int8.focrq. See models/manifest-v2.json.

    Weights license: Baidu Unlimited-OCR-2 - Copyright (c) 2026 Baidu, MIT License.

  3. v0.9.1v0.9.1 — --multi-page keeps text outside <PAGE> markers2026-09-29 · 2026-09-29 02:55 UTC
    查看更新说明

    v0.9.1 is a patch release for --multi-page output. It ships the same six-platform raw-executable matrix as v0.9.0 (each asset with a SHA256 sidecar) and still pins the hash-verified v0.7.0 Unlimited-OCR model artifact. Install with the one-liner; focr pull fetches weights.

    Fixed

    • --multi-page kept only the <PAGE> markers and dropped the text (GH #17). Page assembly followed the reference implementation's split('<PAGE>')[1:], which throws away everything before the first marker. When the model skipped the opening marker (common on single pages), the whole transcription was discarded and the output held only <PAGE>. Text before the first marker is now kept as the first page, a decode with no markers is kept as one page, and the streaming path does the same. (5707079)
    • The progress reporter releases its lock before calling the progress sink, so a sink that reports progress itself can no longer deadlock a run. (c84df0e)

    Changed

    • Built against Asupersync 0.5.0 and FrankenSQLite 0.4.0.

    Build

    • Built by the dsr local release pipeline (not GitHub Actions) from source commit 045e65702fcd3c78187919903103b925e29b31dd, with the pinned sibling closure the lockfile resolves against: asupersync 78b64636, frankentorch 4b23eb8a, frankensqlite 2f401ee7.

    Verification

    • scripts/check.sh validators and installer end-to-end test, cargo fmt --check, cargo check --locked --all-targets, and cargo clippy --locked --all-targets -- -D warnings green; cargo test --locked passed 1,436 tests across 32 suites with no failures.
    • tests/pinned_fixture_hashes.rs now guards the sha256-pinned model manifests and fixtures that a formatting sweep had rewritten on main after 0.9.0 (that regression never shipped).

    Full history: see CHANGELOG.md §0.9.1.

  4. v0.9.0v0.9.0 — tall-capture strip routing and the low-yield guard2026-08-23 · 2026-08-23 03:39 UTC
    查看更新说明

    focr v0.9.0 — tall-capture strip routing and the low-yield guard

    v0.9.0 ships the same six-platform raw-executable matrix as v0.8.0 (each asset with a SHA256 sidecar) and continues to pin the immutable, hash-verified v0.7.0 Unlimited-OCR model artifact (unlimited-ocr-ffn-int8-attn-bf16-lmhead-bf16-v1). Install with the one-liner; focr pull fetches weights.

    Highlights

    • Tall full-page captures work now (GH #15). Browser-extension "capture full page" screenshots (e.g. Awesome Screenshot) produce images 5-20× taller than wide; these previously came back near-empty with exit 0, or hung. Single images at least 3× taller than wide are now automatically OCRed as smart-cut horizontal strips: cut lines are chosen at minimum-ink rows so text lines are never severed, each strip runs through the standard pipeline, and layout boxes are retranslated into whole-image coordinates.
    • Low-yield guard. Any run on an image ≥ 0.5 MP that produces fewer than 50 characters per megapixel is now flagged: the run_complete robot event carries low_yield fields and a stderr warning is printed. Opt-in --fail-on-low-yield turns that condition into new stable exit code 8, so automation can stop treating silent near-empty output as success. The robot schema stays v1 (additive fields); the frozen schema fixture, surface matrix, and parity pins were updated in lockstep.
    • crates.io packaging repair. The published crate now ships models/manifest-v2.json, repairing cargo install builds of the embedded pull manifest.

    Platform notes

    • macOS Apple Silicon, macOS Intel, Linux x86-64 (glibc 2.17+), Linux ARM64 (glibc 2.17+), Windows x86-64, and Windows ARM64.
    • macOS binaries and Linux x86-64 were built and smoke-tested (--version, robot schema|health|backends JSON-validated, robot selftest int8 kernels bit-identical to the scalar oracle) on their build hosts; Windows x86-64 was built natively on Windows and passed the same smoke + selftest. Linux ARM64 was cross-built with the pinned zigbuild glibc-2.17 floor (readelf-verified GLIBC_2.17 max on both Linux assets) but not executed (no ARM64 Linux host in the pipeline). Windows ARM64 was cross-arch built on x86-64 Windows with the MSVC ARM64 toolchain + LLVM (ring's aarch64 path) and, like v0.7.2/v0.8.0, was not executed on ARM64 hardware — full-model OCR verification on physical ARM64 Windows remains a release-verification obligation.
    • Built by the DSR local release pipeline (not GitHub Actions) from source commit 1f7f6698e3513554331e2d83e213db4822e13caf, with the pinned sibling closure: asupersync 76c93637, frankentorch 9627f39c, frankensqlite bda32118.

    Verification

    • cargo fmt --check, cargo check --locked --all-targets, and cargo clippy --locked --all-targets -- -D warnings green at the release commit; the full cargo test --locked suite green on the Linux build host (31 suites, 0 failures).
    • Release-ref gate: scripts/gauntlet_cert.py --dist-ref-preflight PASS for tag v0.9.0 at 1f7f669 (tag == Cargo version, reachable from origin/main).
    • Sibling build inputs pinned and verified at checkout on every host (dist.yml env == repos.d registry): the first frankensqlite pin (339410c) turned out to be a broken-HEAD window upstream and was advanced to the repaired bda3211 before any artifact was produced.
    • Each asset ships with a SHA256 sidecar; sidecars re-verified after transfer to the staging host.
    • The GH #15 tall-capture path is covered by 18 unit tests on synthetic tall images (including the 494×2000 incident shape); a model-level end-to-end run on real full-page captures was NOT part of this gate (no weights on the gate hosts).

    Full history: see CHANGELOG.md §0.9.0.

  5. v0.8.0v0.8.0 — resident warm-model daemon, repaired installer, machine-parseable --version2026-08-20 · 2026-08-20 00:53 UTC
    查看更新说明

    focr v0.8.0 — resident warm-model daemon, repaired installer, machine-parseable --version

    v0.8.0 ships the same six-platform raw-executable matrix as v0.7.2 (each asset with a SHA256 sidecar) and continues to pin the immutable, hash-verified v0.7.0 Unlimited-OCR model artifact (unlimited-ocr-ffn-int8-attn-bf16-lmhead-bf16-v1). Install with the one-liner; focr pull fetches weights.

    Highlights

    • Resident warm-model daemon (GH #9). focr ocr now serves eligible single-image runs from a per-model background process that keeps the loaded weights in RAM, so back-to-back invocations skip the multi-gigabyte artifact load. Loopback TCP with a user-only (0600) token state file, strictly serial service, idle auto-exit after 10 minutes (FOCR_RESIDENT_IDLE_SECS). Every request re-verifies binary version, model identity, artifact mtime/length, load-resolved decode/preprocess options, and an inference-environment fingerprint; any transport-shaped problem silently falls back to the classic in-process load, so the output contract is unchanged. Opt out with --no-resident or FOCR_NO_RESIDENT=1.
    • Installer repair (GH #12). install.sh / install.ps1 no longer trust GET /releases/latest (which broke every fresh install once a models-* weights release became the newest release): both now enumerate /releases and select the newest semver v* tag.
    • Machine-parseable --version (GH #13). stdout carries only focr <semver>; the license attribution lines moved to stderr.
    • Unlimited timeout escape hatch (GH #10). FOCR_STAGE_BUDGET_FORWARD_MS=0 (or unlimited) disables the forward stage budget instead of being silently ignored.
    • Agent-facing robot upgrades. run_error events carry a machine-consumable recovery field; focr robot schema self-describes the steering environment variables, an agent_discovery command map, and the stdout contract; root --help points agents at focr robot triage.
    • Browser wasm + iOS lanes matured in this window (wasm simd128 int8/int4 kernel island, streaming loader, iOS staticlib + app scaffolding) alongside the Hugging Face weight mirror; see CHANGELOG for details.

    Platform notes

    • macOS Apple Silicon, macOS Intel, Linux x86-64 (glibc 2.17+), Linux ARM64 (glibc 2.17+), Windows x86-64, and Windows ARM64.
    • Unix binaries and Windows x86-64 were built natively and smoke-tested on their build hosts. Windows ARM64 in this release was cross-compiled (clang-cl/MSVC-target via cargo-xwin) rather than built on a native ARM64 runner; like v0.7.2 it carries a narrower proof than the other targets — full-model OCR verification on physical ARM64 Windows hardware remains a release-verification obligation.
    • Built by the DSR local release pipeline (not GitHub Actions) from source commit 8ec5343a8996c9fdebe447a52f48682334e13bfa, with the pinned sibling closure: asupersync 73e00b34, frankentorch 14ae04bc, frankensqlite 065cafc3.

    Verification

    • cargo fmt --check, cargo check --locked --all-targets, cargo clippy --locked --all-targets -- -D warnings, and the full cargo test --locked suite green at the release commit (28 suites; the one initially failing surface-ledger enumeration test was fixed in-tree before tagging).
    • tests/installer_e2e.sh: ALL GATES PASS against the new release-resolution logic.
    • Each Unix asset ran --version, robot schema (JSON-validated), and focr robot selftest (int8 kernels bit-identical to the scalar oracle) on its build host before staging; Linux assets passed the readelf glibc-2.17 floor check.

    Full history: see CHANGELOG.md §0.8.0.

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    franken_ocr

    License: MIT source tag: v0.10.0 binary release: v0.10.0 status: working Rust Edition toolchain: nightly unsafe: forbidden* default model: Baidu Unlimited-OCR v0.7.0: release payload verified

    A pure-Rust, memory-safe, CPU-only OCR engine for a small family of hand-ported vision-language models. Baidu Unlimited-OCR is the fast default for document OCR, GOT-OCR2 handles specialized structured formats, SmolVLM2 handles image description and VQA, OneChart extracts chart data, and Polyphonic-TrOMR turns full scanned sheet-music pages or staff crops into MusicXML through --task music. All five runtime models are available through focr pull; TrOMR publishes both the 61 MB int8 default artifact and an 86 MB f32 reference artifact. The v0.7.0 Unlimited-OCR artifact uses the conservative exact recipe and passes the hard-page termination and complete 20-page corpus budget. The models run through model-specific Rust kernels and need no general ML framework, Python, CUDA, FFI at inference, or GPU.

    curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_ocr/main/install.sh | bash
    

    Windows (native, PowerShell):

    irm https://raw.githubusercontent.com/Dicklesworthstone/franken_ocr/main/install.ps1 | iex
    

    The installer detects your platform, resolves the latest published GitHub binary release (currently v0.10.0), verifies the downloaded asset by SHA256, and puts focr on your PATH. Install compatible model weights separately; after they are present, inference is offline.

    Current Release

    v0.10.0 ships raw executables for macOS Apple Silicon, macOS Intel, Linux x86-64, Linux ARM64, Windows x86-64, and Windows ARM64, each with a SHA256 sidecar. Its embedded schema-v2 manifest continues to pin the v0.7.0 4,157,448,783-byte Unlimited-OCR artifact using recipe unlimited-ocr-ffn-int8-attn-bf16-lmhead-bf16-v1; focr pull verifies all three part hashes and the reassembled file before installation.

    The exact-recipe artifact emits EOS on page0590 and passes the complete 20-page corpus budget at aggregate normalized CER 0.19307925. The release does not claim the strict three-party OpenPGP certificate: the committed fail-closed finalizer requires three independently controlled registry-pinned signers and production audit receipts, which are not available in this release process. The local corpus, model-census, installer, and binary checks are evidence, not a substitute for that governance claim.


    TL;DR

    The problem. Baidu Unlimited-OCR is a strong document-parsing model: Markdown, tables, LaTeX, reading order, many pages in one pass. The official stack is Python plus CUDA. Most machines that need OCR (laptops, CI runners, agent hosts, edge boxes) have no usable GPU, and a Python plus CUDA dependency is heavy to ship and awkward to embed.

    The solution. franken_ocr is a library plus a single-binary CLI (focr) that runs the ready model set on CPU with nothing but a Rust binary. It transforms bf16 checkpoints into custom .focrq int8 artifacts and runs them through kernels written for each model's exact shapes. A historical real-page Unlimited-OCR run measured end-to-end character-error-rate 0.0094 and matched the reference decode to within a single token. The v0.7.0 corpus receipt records 20/20 pages within budget and a still-high page0590 tail; the release publishes that measured artifact without claiming the unavailable three-party signing certificate.

    Why focr?

    FeatureWhat it does
    One portable binaryNo Python, no CUDA, no FFI at inference, no GPU. Current release assets are about 13 to 17 MB; portable to hosts where ort/CUDA cannot build.
    Works offlineOnce compatible weights are installed, inference never touches the network. Zoo pulls install under ~/.cache/franken_ocr/models; the v0.7.0 default is the versioned conservative Unlimited-OCR artifact. Weight bytes are owned by default; trusted immutable deployments can opt into mmap with FOCR_MMAP=1.
    Embeddable Rust APIOcrEngine exposes synchronous, blocking calls for Markdown, structured layout, figure extraction, in-memory images, and load-once batches.
    Native PDFs and figuresScanned PDFs are rasterized in process with pure Rust; page /Rotate and image-placement rotations are honored, --pages selects exact PDF pages, --split-spreads can split two-page book scans, and --extract-figures saves chart/photo regions beside the Markdown or JSON output.
    Cross-page parsing--multi-page runs the Unlimited-OCR infer_multi contract over selected PDF pages, a directory of page images or an image list, producing one document with <PAGE> boundaries instead of unrelated page parses.
    Model zoo with compatibility statusReady engines: Unlimited-OCR, GOT-OCR2, SmolVLM2, OneChart, and Polyphonic-TrOMR, including full-page sheet-music OMR. focr models reports compatible and blocked pull entries with recipe metadata. All five runtime models are pullable in v0.7.0. Planned descriptors: TrOCR and pix2tex.
    Measured int8 decode pathHistorical rows show the custom int8 kernels can accelerate decoder work. The v0.7.0 exact-recipe artifact passes the hard-page termination and corpus-CER budget. Raw BF16 is only an all-high-precision native diagnostic when paired with FOCR_DECODE_STATELESS=1; the default cached decode applies the conservative int8 FFN/expert cache regardless of source storage.
    Runtime ISA dispatchOne binary per architecture detects every available int8 ISA tier and reports the effective ordinary dense-GEMM route separately. Apple Silicon currently defaults to measured-faster LLVM autovec while retaining forced SDOT/SMMLA proof paths; x86 selects AVX-512-VNNI, AVX-VNNI, AVX2, or scalar.
    Measured zoo gauntletdocs/PERF_LEDGER.md records paired HF CPU reference rows. In the historical forced-Apple-SDOT rows at thread parity, decode-per-token ratios are 3.37x for GOT-OCR2, 2.58x for OneChart, and 1.67x for SmolVLM2; end-to-end rows are also kept, including slower cases.
    Batch throughput pathfocr ocr-batch loads weights once; the optional continuous-batch spine can prefill/decode multiple pages together while preserving per-page bytes. The dense zoo path covers GOT-OCR2, SmolVLM2, and OneChart with per-page token budgets. SAM, CLIP, and SigLIP towers plus their connector/embed tables are hydrated once, then reused across pages and batches.
    Stage timing instrumentationFOCR_TIMING=1 reports nested forward timings, including SAM hydrate/forward/block/attention/MLP splits, so perf work can separate artifact load, vision attention, decode, and output costs.
    Durable run historyfocr ocr records best-effort local telemetry in fsqlite; focr runs and focr sync expose the run log as plain text, JSON, NDJSON, or locked JSONL.
    Bounded long-doc memoryR-SWA keeps generated-token KV constant (window 128) while the reference block is held as a frozen, never-evicted global KV.
    Agent-firstVersioned NDJSON robot mode, a self-describing robot schema, one-shot robot triage, TrOMR staff and music_warning events for music runs, stable documented exit codes, deterministic output under fixed sampling.
    Provable kernelsfocr robot selftest re-runs the dispatched int8 GEMM against a bit-identical scalar oracle on your CPU, including per-model verdicts for Unlimited-OCR, GOT-OCR2, SmolVLM2, and OneChart.
    Real-scan music gateThe TrOMR lane has public-domain Spohr page/staff fixtures, human-verifiable attributes, a frozen MusicXML anchor, and a model-gated NDJSON runner so real engraved scans are measured separately from synthetic examples.
    Release evidencescripts/ladder_scorecard.sh folds the L0-L5 parity ladder, docs/FEATURE_PARITY.md accounts the surface area, tests/property_suite.rs exercises generator-driven invariants, the committed fuzz corpus seeds four libFuzzer targets, scripts/bench_guardrail.py compares stage timings against frozen baselines, and scripts/gauntlet_cert.py computes the three-pillar scorecard, invariant monitors, and 13/13 release-readiness gate.
    Memory-safe#![forbid(unsafe_code)] everywhere except small audited islands: SIMD kernels with bit-identical scalar fallbacks, plus the explicitly opted-in read-only mmap loader.

    System Map

    LayerWhat is live today
    InputsPNG/JPG-style images, scanned PDFs, selected PDF page ranges, split book spreads, image batches, full sheet-music pages, and single staff crops.
    Model routingUnlimited-OCR for general documents, GOT-OCR2 for structured OCR modes, SmolVLM2 for describe/VQA, OneChart for chart data, and TrOMR for MusicXML.
    Runtime coreOcrEngine owns one asupersync runtime, caches one model per process, exposes blocking Rust calls, and records best-effort run telemetry through fsqlite.
    CPU executionFixed-shape Rust forwards call scalar-or-proven SIMD kernels. Apple Silicon advertises SDOT/SMMLA hardware but uses measured-faster LLVM autovec for ordinary dense int8 by default; Intel/AMD selects AVX-512-VNNI, AVX-VNNI, AVX2, or scalar.
    OutputsMarkdown, structured JSON with layout boxes, cropped figure assets, MusicXML, versioned robot NDJSON, run-history JSON/NDJSON/JSONL, and release evidence artifacts.

    Quick Example

    # 1. Install the versioned conservative Unlimited-OCR artifact.
    focr pull
    
    # 2. OCR a page into Markdown (the human default).
    focr ocr page.png
    
    # 3. Same page as structured JSON (markdown + every span's bounding boxes).
    focr ocr page.png --json
    
    # 4. Write the result to a file; format follows the extension (.md or .json).
    focr ocr page.png -o page.md
    focr ocr page.png -o page.json            # structured JSON with bounding boxes
    
    # 5. Also save figures the model can't transcribe (charts/photos) next to the .md.
    focr ocr page.png -o page.md --extract-figures   # -> page.md + page_figures/
    
    # 6. Use a specialized model when the task is not plain document OCR.
    focr pull got-ocr2
    focr ocr --model got-ocr2.int8.focrq --task tables table.png
    
    focr pull smolvlm2
    focr ocr --model smolvlm2.int8.focrq --task describe photo.jpg
    focr ocr --model smolvlm2.int8.focrq --task describe --question "How many people are visible?" photo.jpg
    
    focr pull onechart
    focr ocr --model onechart.int8.focrq --task chart-data chart.png
    
    focr pull tromr
    focr ocr --model tromr.int8.focrq --task music score-page-or-staff.png -o score.musicxml
    focr pull tromr --quant f32                 # bit-exact reference artifact when needed
    focr ocr --model tromr.focrq --task music score-page-or-staff.png -o score.musicxml
    
    # 7. OCR only the pages you need from a scanned PDF; optionally split two-page spreads.
    focr ocr book.pdf --pages 3,5-9 --split-spreads -o excerpt.md
    
    # 8. Stream NDJSON pipeline events for an agent (music runs also emit `staff` events).
    focr ocr page.png --robot
    focr robot triage
    
    # 9. Prove the int8 kernels on THIS CPU are bit-identical to the scalar oracle.
    focr robot selftest
    
    # 10. Generate the parity-ladder and release-readiness scorecards used by release gates.
    scripts/ladder_scorecard.sh --self-test
    scripts/ladder_scorecard.sh --out scorecard.json
    python3 scripts/gauntlet_cert.py --self-test
    python3 scripts/bench_guardrail.py --self-test
    python3 scripts/gauntlet_cert.py --from-parity docs/FEATURE_PARITY.md \
      --scorecard-out /tmp/focr-release-scorecard.json
    # From a clean final source/evidence commit, produce the red provisional bundle.
    # It exits 1 until fresh CI artifacts and three trusted signers finalize it.
    python3 scripts/gauntlet_cert.py --bundle .gauntlet-output/bundle
    # After downloading the exact CI, dist, Model Parity, and Performance Gauntlet
    # artifacts for this HEAD, run --finalize-bundle as documented in
    # docs/gauntlet/RELEASE_CERTIFICATION_TEMPLATE.md.
    python3 scripts/gauntlet_cert.py --release-readiness \
      --readiness-out .gauntlet-output/release_readiness.json
    
    # 11. (optional) Convert the exact pinned Unlimited-OCR bf16 shard.
    # Same-shaped or reserialized inputs fail before an unloadable artifact is written.
    focr convert model.safetensors -o unlimited-ocr.focrq --quant int8
    
    # TrOMR conversion is available for local sheet-music artifacts.
    focr convert /path/to/tromr/model.safetensors -o tromr.int8.focrq --quant int8 --model-id tromr
    

    With FOCR_MODEL_PATH set, default-model inference runs fully offline. Compatible specialized pulls live under ~/.cache/franken_ocr/models and are also offline after installation.


    Design Philosophy

    A few models, every dimension fixed. A general ML framework pays a generality tax on every operation: dynamic dtype dispatch, arbitrary shapes, autograd bookkeeping, broadcast machinery, and a device abstraction. franken_ocr runs a small set of hand-ported models whose important dimensions are known up front. For the default Unlimited-OCR path that means hidden 1280, 10 heads, head_dim 128, 64 experts, top-6 routing, MoE intermediate 896, R-SWA window 128, and vocab 129280. That buys shape-specialized kernels with no runtime shape branching in the hot loop. The scope is a few hand-tuned, certified models, not any model; there is no generic runtime underneath.

    Model status is explicit. focr models is the source of truth for the runtime zoo. Ready models have a runtime forward arm and can be used with focr ocr; planned models are visible so agents and humans can see the roadmap without accidentally running the wrong architecture. The table distinguishes runtime readiness from distribution compatibility. Unlimited-OCR, GOT-OCR2, SmolVLM2, OneChart, and TrOMR are pullable in v0.7.0. Non-primary models install into per-model cache subdirectories with their tokenizer sidecars beside the .focrq artifact. TrOMR publishes both int8 and f32 artifacts: the default pull is the smaller int8 artifact, while focr pull tromr --quant f32 restores the bit-exact reference path for audits.

    Scanned books are addressable. PDF input no longer means "the whole document or nothing." --pages 3,5-9 selects source pages with 1-based ranges, reports out-of-range requests against the document page count, and keeps source page numbers in JSON/robot output. The native renderer applies both PDF page /Rotate metadata and the rotation implied by the image placement matrix, which fixes scanned-book files that store portrait page images but display them landscape through a rotated cm transform. --split-spreads is opt-in for common two-page book scans: a wide rasterized page with a near-blank center gutter becomes left and right logical pages, while pages without a qualifying gutter pass through unsplit.

    TrOMR page OMR is resilient. TrOMR accepts either a staff crop or a full printed/scanned page. The page path runs pure-Rust staff detection with global deskew, trims horizontal page margins to the detected ink extent, grows wide staff bands toward neighbor midlines so they fit the model's 1280-column position budget where possible, orders crops top-to-bottom, recognizes each staff sequentially through the certified ResNetV2 plus ViT encoder and four-head decoder, merges the semantic streams, and emits partwise MusicXML. If a band is still too wide, setting FOCR_TROMR_SPLIT=1 arms an experimental rescue that detects thin full-span barlines, splits the band into in-budget segments, and splices the streams (off by default: isolated segments measurably lose absolute pitch registration, so this trades content fidelity for recognition coverage; the skip with a named reason is the honest default). If one detected staff fails, the page still succeeds with the staves that recognized and logs the skipped staff's bbox and reason; if every staff fails, the error names each staff reason. JSON output includes the ordered staves array for music runs, and robot mode emits one staff event for each recognized or skipped staff before run_complete. The MusicXML emitter also adds annotate-only sanity comments for overfull bars, underfull middle bars, impossible durations, and cross-staff key mismatches; JSON and robot mode expose the same observations as warnings and music_warning events. Remaining TrOMR work is narrower: optional **kern export, camera-photo dewarp, and broader corpus-quality metrics.

    Performance claims are ledgered. The A11 zoo gauntlet records native runs beside pinned Hugging Face CPU references for GOT-OCR2, SmolVLM2, and OneChart. Decode-per-token speedups are documented where the native path is ahead, and full end-to-end rows stay in the ledger even when artifact loading or preprocessing makes the total slower. The CPU backend also records losing rows, such as the f32 TrOMR baseline, instead of turning them into marketing claims. The README summarizes measured rows; docs/PERF_LEDGER.md is the audit trail.

    Offline at inference. Network access is limited to explicit compatible pull commands; inference itself never downloads. Raw BF16 or locally converted Unlimited weights can be supplied through FOCR_MODEL_PATH. There is no Python, no CUDA, no FFI, and no GPU in the inference path. The async runtime that orchestrates I/O and cancellation is an owned internal detail; the library API is synchronous and blocking, so there is no async plumbing to thread through your code.

    Correctness before speed (always). A parity gate comes first and a faster kernel that drifts the OCR output is reverted, no source landed, and recorded in the negative-evidence ledger. Speed is shipped on top of parity, never instead of it. The conservative recipe keeps the vision tower, projector, embeddings, attention, lm_head, MoE router, and all norms high precision. The v0.7.0 artifact passes page0590 termination and the 20-page corpus budget, with the hard-page CER tail documented. Those measurements do not imply the separate three-party signing certificate.

    Conformance has receipts. The L0-L5 parity ladder emits structured NDJSON, and scripts/ladder_scorecard.sh folds that stream into one focr-ladder-scorecard/v1 artifact. An unarmed no-weights run is recorded as skipped_no_model:true, never as green. docs/FEATURE_PARITY.md splits the release surface into the numbered FeatureUniverse and the CLI/robot SurfaceMatrix; tests/surface_matrix.rs fails if a live subcommand, robot event, exit code, or rollup cell is missing from that ledger. The three-pillar gauntlet in docs/gauntlet/METHODOLOGY.md and scripts/gauntlet_cert.py turns those rows into a franken_ocr.gauntlet.scorecard.v1 artifact, while the release ratchet compares lower bounds rather than point estimates. A change that improves the aggregate while lowering one category is blocked.

    Runtime ISA dispatch, one binary per arch. There is no per-CPU-feature variant to choose. One x86_64 binary covers AVX2 / AVX-VNNI / AVX-512-VNNI; one aarch64 binary covers NEON / SDOT / SMMLA. Hardware capability and the effective ordinary dense-int8 route are distinct: Apple Silicon exposes SDOT/SMMLA, but measured decode results select the LLVM-autovectorized scalar loop by default. Forced SDOT and SMMLA remain available for parity and benchmark sweeps. Non-Apple ARM64 can select SMMLA when it is actually faster. On a Threadripper 5995WX, a Zen 3 part whose ceiling is AVX2, dispatch correctly selects AVX2.

    Bounded generated-token KV. Every decoder attention layer is R-SWA (Reference Sliding Window Attention). Each generated token attends to all reference tokens (visual plus prompt prefix, kept as a frozen, never-evicted global KV) plus only the previous 128 generated tokens through a ring-buffer KV cache. Generated-token KV memory stays constant instead of growing with output length. That, not arbitrary input resolution, is what "Unlimited" means.


    How franken_ocr Compares

    franken_ocr is the only one of these built for a fixed, hand-tuned set of models on CPU.

    franken_ocr v0.10.0Official Unlimited-OCRllama.cppONNX Runtime
    Language / runtimePure Rust, one binaryPython + HF transformersC++C++
    Primary targetCPUCUDA GPUCPU/GPUCPU/GPU
    ScopeA few hand-tuned modelsThis modelMany modelsMany models
    int8 kernelsModel-specific tiled SDOT/SMMLA/VNNIn/aGeneric K-quantMLAS
    Vision encoderFirst-class, kept high precisionFirst-classKept F16 (mmproj)Depends on export
    Network at inferenceNoneNoneNoneNone
    Ships asSingle static binary, no FFIPython env + CUDABinary + modelLibrary + model
    Constant generated-token KVYes (R-SWA preserved)YesDepends on PR supportDepends
    Runs with no GPUYesNo (needs CUDA)YesYes

    When to use franken_ocr:

    • You need this model's output and your host has no usable GPU.
    • You want to embed OCR in a Rust program with no Python or FFI.
    • You want one binary you can drop on a CI runner, an agent host, or an edge box.

    When franken_ocr might not be ideal:

    • You need a generic inference runtime that loads arbitrary models. franken_ocr runs a few hand-ported, certified models by design.
    • You need the OmniDocBench leader; Unlimited-OCR is strong but not the top of the board.

    Installation

    Quick install (recommended)

    curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_ocr/main/install.sh | bash
    

    The script detects your OS and CPU architecture, downloads the matching asset from the latest published binary release, verifies the SHA256 sidecar, and installs focr. At the time this source tag was cut, the latest published binary release was v0.10.0. Under WSL it proceeds as Linux. Under native Git-Bash, MSYS, or Cygwin it points you at the PowerShell installer below and exits cleanly.

    On native Windows, install from PowerShell:

    irm https://raw.githubusercontent.com/Dicklesworthstone/franken_ocr/main/install.ps1 | iex
    

    This selects focr-x86_64-pc-windows-msvc.exe on AMD64 or focr-aarch64-pc-windows-msvc.exe on ARM64, verifies it by SHA256, and puts focr on your PATH. Both installers stage and execute the verified binary before an atomic replacement while holding a destination-scoped process lock, so a failed or concurrent update does not clobber a working install. Both Windows architectures are part of the v0.10.0 release matrix.

    Homebrew (macOS / Linux)

    brew install dicklesworthstone/tap/focr
    

    The formula installs the same SHA256-pinned release binary for your platform.

    Manual binary download

    Release binaries are raw executables, not tar.gz archives. Each one is a single portable file that dispatches the ISA tier at runtime, so there is one binary per architecture (no per-CPU-feature variant). Linux assets are GNU binaries linked for glibc 2.17 or newer; the dist producer independently replays readelf against the exact staged bytes and refuses a higher required symbol version.

    PlatformAsset
    macOS Apple Siliconfocr-aarch64-apple-darwin-neon-sdot-i8mm
    macOS Intelfocr-x86_64-apple-darwin
    Linux x86-64 (glibc 2.17+)focr-x86_64-unknown-linux-gnu
    Linux ARM64 (glibc 2.17+)focr-aarch64-unknown-linux-gnu
    Windows x86-64focr-x86_64-pc-windows-msvc.exe
    Windows ARM64focr-aarch64-pc-windows-msvc.exe

    Each asset has a <asset>.sha256 sidecar in the standard "<hex> <asset>" format. Download the binary and its sidecar from the release base URL, verify, then install. Example for Apple Silicon:

    base=https://github.com/Dicklesworthstone/franken_ocr/releases/download/v0.10.0
    asset=focr-aarch64-apple-darwin-neon-sdot-i8mm
    
    curl -fsSLO "$base/$asset"
    curl -fsSLO "$base/$asset.sha256"
    
    # macOS: shasum -a 256 -c   |   Linux: sha256sum -c
    shasum -a 256 -c "$asset.sha256"
    
    chmod +x "$asset"
    mv "$asset" /usr/local/bin/focr
    

    On Linux, swap the asset name and use sha256sum -c "$asset.sha256".

    From source (advanced; not a one-liner)

    franken_ocr requires the nightly Rust toolchain pinned in rust-toolchain.toml. cargo build --locked --release builds both the focr and franken_ocr binaries from one shared entrypoint.

    As of v0.10.0 the crate and its dependency closure (frankentorch-core, frankentorch-kernel-cpu, the fsqlite family, asupersync) are published on crates.io, so franken_ocr can be used as a library dependency. Two caveats: the crate requires the pinned nightly toolchain on aarch64 (stdarch feature gates in lib.rs), so cargo install franken_ocr fails on a stable default toolchain there, and the registry build is not the byte-exact release binary. Prebuilt binaries remain the supported install path. A source checkout build still expects the sibling repositories (../asupersync, ../frankentorch, ../frankensqlite) laid out beside it (the workspace path-deps win over the registry).

    cargo build --locked --release
    # produces target/release/focr and target/release/franken_ocr (identical behavior)
    

    Quick Start

    1. Install the binary with the curl one-liner, or download and verify a release asset by hand.
    2. Install compatible weights: run focr pull for the versioned conservative Unlimited-OCR artifact. Long documents can use focr pull unlimited-ocr-2; specialized models use focr pull got-ocr2, focr pull smolvlm2, focr pull onechart, or focr pull tromr. Raw BF16 and locally converted exact-recipe artifacts remain available through FOCR_MODEL_PATH.
    3. Verify your CPU kernels (optional but reassuring): focr robot selftest. Exit 0 means the dispatched int8 GEMM cases, including the model-specific rows for Unlimited-OCR, GOT-OCR2, SmolVLM2, and OneChart, are bit-identical to the scalar oracle on this host.
    4. OCR a page: focr ocr page.png for Markdown, add --json for structured output (markdown + bounding boxes), -o out.md / -o out.json to write a file, or --robot for an NDJSON event stream.
    5. Batch many pages in one process (model loaded once): focr ocr-batch page1.png page2.png page3.png --json. Add --multi-page to parse them as ONE cross-referencing document instead of independent pages.

    Library API

    franken_ocr is a library as well as a CLI. The public API is synchronous and blocking: OcrEngine owns the internal asupersync runtime, loads the model on first use, caches that model behind an Arc, and returns ordinary FocrResult<T> values. Callers do not need to build or pass an async runtime.

    use std::path::Path;
    
    use franken_ocr::{OcrEngine, FocrResult};
    
    fn main() -> FocrResult<()> {
        let engine = OcrEngine::new()?;
    
        let markdown = engine.recognize(Path::new("page.png"))?;
        println!("{markdown}");
    
        let document = engine.recognize_with_layout(Path::new("page.png"))?;
        println!("{} layout spans", document.layout.len());
    
        Ok(())
    }
    

    The same engine exposes the surfaces the CLI uses:

    MethodUse case
    recognize(path)Return Markdown for one image or document page.
    recognize_with_model(model, path)Pin a specific .focrq artifact without changing environment variables.
    recognize_with_layout(path)Return Markdown plus grounded bounding boxes, matching focr ocr --json.
    recognize_with_figures(path)Return Markdown/layout plus cropped figure regions, matching --extract-figures.
    recognize_dynamic(image)Run an already-decoded image::DynamicImage, the same in-memory path used by PDF rasterization.
    recognize_batch(&[paths])Load weights once and return one result per input path, in order.
    recognize_document(model, source, &options, on_item, on_event)Parse a PDF, a directory or a list of pages as ONE cross-page document, matching --multi-page. The PageSource is read one page at a time; on_event streams each page as it ends, and each decode window (DocumentEvent). DocumentOptions picks the decode windows (Unlimited-OCR-2 reads windows of 10 pages by default; PageWindow::Off is one pass). The DocumentResult has the markdown, the pages, the skipped pages, the windows and the finish_reason. A document that stops early returns the pages it completed.
    recognize_pages(model, source, observer)Read a PageSource one page at a time, each page its own forward, matching focr ocr book.pdf. A failed page is skipped and reported to observer; a missing model or a cancel ends the walk.
    request_shutdown() / reset_shutdown()Drive cooperative cancellation for long-running embedders.

    OcrEngine belongs to the native feature. A program that wants the engine alone links default-features = false, features = ["library"], which brings no CLI, async runtime, run store, signal handler or network path, and calls OcrModel directly. docs/library-output.md is its versioned contract: page and span fields, box coordinates, reading order, side effects and what is not promised.

    A document parse, using the pages of a PDF:

    use std::path::Path;
    
    use franken_ocr::page_source::PageSource;
    use franken_ocr::{DocumentOptions, FocrResult, OcrEngine};
    
    fn main() -> FocrResult<()> {
        let engine = OcrEngine::new()?;
        let source = PageSource::pdf(Path::new("book.pdf"))?.select(Some("1-20"))?;
        let document = engine.recognize_document(
            &OcrEngine::model_path(),
            source,
            &DocumentOptions::default(),
            Box::new(|_| {}),
            Some(Box::new(|page| eprintln!("page {} done", page.index))),
        )?;
        println!("{}", document.markdown);
        eprintln!("{} pages, finish: {}", document.pages.len(), document.finish_reason.as_str());
        Ok(())
    }
    

    Model resolution follows the CLI rules: FOCR_MODEL_PATH wins, then the default cache location is searched. For path-explicit calls, pass the model artifact directly. The engine keeps the one-live-forward discipline from the CLI, so a single process can reuse weights without fanning out concurrent model forwards.


    Command Reference

    Both focr ocr and focr robot run accept the same image plus inference-tuning flags. The default crop mode is base (the certified single 1024-pixel global view); --crop-mode gundam selects the reference dynamic-resolution tiling, whose end-to-end certification is still pending.

    focr ocr <image>

    Parse a document image into Markdown (default), JSON, or an NDJSON stream.

    focr ocr page.png                          # Markdown to stdout
    focr ocr page.png --json                   # structured JSON to stdout
    focr ocr page.png -o page.md               # write Markdown to a file
    focr ocr page.png -o page.json             # write structured JSON (markdown + boxes) to a file
    focr ocr page.png -o page.md --extract-figures   # also save figures, referenced from the .md
    focr ocr page.png --robot                  # NDJSON pipeline events
    focr ocr page.png --crop-mode gundam       # reference dynamic-resolution tiling (uncertified)
    focr ocr page.png --max-length 4096 --temperature 0.0
    focr ocr page.png --model /path/to/unlimited-ocr.focrq
    focr ocr eq.png --task formula --model got-ocr2.int8.focrq   # specialized task routing (see below)
    focr ocr chart.png --task chart-data --model onechart.int8.focrq
    focr ocr book.pdf --pages 3,5-9 --split-spreads -o excerpt.md
    focr ocr ./scans/ --multi-page -o book.md  # a directory of page images as one document
    curl -s "$URL" | focr ocr - -o page.md     # an image or a PDF on stdin
    focr ocr book.pdf --multi-page --check     # is this run ready? about a second, no weights loaded
    focr ocr page.png --raw -o page.raw.txt    # the model's output, grounding markers included
    focr ocr page.png --threads 8 --timing     # eight kernel threads, stage timings on stderr
    

    Stdin (-). focr ocr - reads an image or a PDF from stdin, whichever the bytes are. The run holds them in a private temp file and removes it when it ends. A file actually named - is ./-. Empty stdin, or bytes that are neither, exits 4.

    Preflight (--check). Runs the checks that can fail before a decode, then stops: the arguments, the input's header or page list, --pages, the model's header and tokenizer, and that -o has a directory to write into. It takes about a second on the real artifacts, exits with the code the run would fail with, and records nothing in focr runs. --json prints {command: "ocr.check", ok, input, model, output}. It does not decode pixels, so a truncated image or a PDF page that will not rasterize passes here and fails in the run. Use --json, not --robot, with it.

    Output (-o/--output FILE). Writes the result to a file instead of stdout; the format follows the extension: .json emits structured JSON, any other extension (e.g. .md) emits Markdown. --json forces JSON regardless of extension. The structured JSON carries the rendered markdown plus a layout array, one {label, boxes} entry per grounded span, where each box is [x1, y1, x2, y2] in source-image pixels. A span adds text, what the model wrote for that region: the text after its marker, up to the next span, normalized as the markdown is. Image spans have no text, and neither does a region the model boxed without writing anything. An Unlimited-OCR-2 title adds its heading level (1, 2, …). A PDF nests these under a per-page pages array; split spreads become separate logical page entries with the same source page number and a "half": "left" or "right" marker. This is the same shape --json prints to stdout. TrOMR music runs also include a staves array in source order; each entry carries the 1-based staff number, page-space bbox, recognized or skipped status, and an optional skip reason.

    Raw output (--raw). Outputs the decode as the model wrote it, grounding markers included: <|det|>label [x1, y1, x2, y2]<|/det|> spans (or the <|ref|> form), or Unlimited-OCR-2's <BLOCK>label[x1, y1, x2, y2]<FORK> prefixes. It replaces the markdown on stdout and in a text -o file. With JSON it adds a raw string beside the markdown and to each entry of pages, and with --robot to run_complete. Boxes stay on the model's 0–999 grid, relative to each page, or to each strip of a tall image, so use layout for source-image pixels.

    Figures (--extract-figures). The model sees figures/photos/diagrams it does not transcribe to text. With --extract-figures, those regions are cropped out of the source image and saved into a subfolder (default <output-stem>_figures/, or set --figures-dir DIR), then referenced from the output: the Markdown gets a real ![figure N](https://github.com/Dicklesworthstone/franken_ocr/blob/HEAD/<output-stem>_figures/page1_figure_1.jpg) in place of each figure, and the JSON gains a figures array of {label, page, bbox, path}. Each figure's format is chosen by content: JPG (quality 85) for photographic regions, lossless PNG for line-art / charts / screenshots. Requires -o (or --figures-dir for a stdout run). PDFs name figures per page (page{N}_figure_{M}); an Unlimited-OCR-2 document with --split-spreads names a half's figures page{N}l_… / page{N}r_….

    Directory input. focr ocr ./scans/ reads a directory of page images (.png, .jpg, .jpeg, .tif, .tiff, .webp, .bmp, .gif, .pnm and its variants) as one document, in natural order: p2.png comes before p10.png. Each image's EXIF orientation is applied, as for a single image. Hidden files and subdirectories are passed over. Any other file is left out and reported once as an ignored_files warning (on stderr, run_complete and --json). Everything below that applies to a PDF's pages applies to a directory's images.

    PDF controls. --pages SPEC is for PDFs and directories only. SPEC is a comma-separated list of 1-based pages and inclusive ranges, such as 3, 3-7, or 1,5-9,218; selected pages run in source order with duplicates removed. Out-of-range pages are usage errors that name the document page count. Before OCR, the pure-Rust PDF path applies both the page /Rotate entry and any axis-aligned rotation from the content stream's image placement matrix, so scans stored sideways but displayed upright are normalized before OCR or spread splitting. --split-spreads is also off by default and applies only to documents. It looks for wide raster pages with a near-blank vertical gutter near the center, then OCRs the left and right halves as separate logical pages; no qualifying gutter means the page remains unsplit. Pages are rasterized one at a time as the run reaches them, so a long book never holds every page image in memory.

    Runaway pages (v1). A v1 page that never ends would decode to the token limit (32,768) and come back mostly repetition. The runaway guard stops it instead: past 8,192 tokens it checks the last 2,048 every 256, and three repeating checks in a row end the decode. The text before that point is written, the run reports finish_reason: runaway and exits 5, and ocr-batch marks that image failed and goes on. In robot mode the text arrives as page events (status: decoded) before the run_error. FOCR_RUNAWAY_GUARD=0 turns it off and decodes to the limit, as the reference does. On ground truth it fires on no OmniDocBench page and on 9 of 18,118 MarathonDocBench pages, 7 with broken references and 2 long charts (DISC-023).

    Multi-page cross-page parsing (--multi-page). By default every page is parsed independently. --multi-page instead parses the whole document across pages using the Unlimited-OCR infer_multi contract, so page N can reference content on the pages before it, and the output is a single Markdown document with <PAGE> separators. On focr ocr it takes a PDF or a directory of page images and composes with --pages; for an image list use focr ocr-batch page1.png page2.png --multi-page. v1 has 32,768 positions shared by prompt and output (≈29 dense pages); longer documents stop early and report finish_reason: length with a v1_truncated warning (a prompt that alone exceeds the budget fails with an actionable error). When v1 ends with fewer page bodies than pages, a v1_page_merge warning says the page boundaries are unreliable; with more bodies than pages, a v1_extra_pages warning says it repeated or invented pages. On v1 it does not compose with --split-spreads or --extract-figures (per-page semantics), and --json lists real pages[] only when the text has exactly one body per page (otherwise pages: []).

    --page-window N|auto|off sets how unlimited-ocr-2 splits a --multi-page document into decode windows. N is pages per window, at most 102, and the default is 10. auto picks the largest window, up to 10, whose estimated whole-process memory fits FOCR_MEMORY_BUDGET, and refuses with the estimate's parts when not even one page fits. off reads one pass with no checkpoint, so a long document can stop at the position limit with its pages written and exit 5. Robot mode reports a window event after each window's pages, and a terminal shows pages and windows done, tokens per second and an estimate of the time left.

    Unlimited-OCR-2 (--model unlimited-ocr-2) emits exactly one <PAGE> per page, so its document mode has real pages. A document of more than 10 pages is read in windows of 10: each window's prompt holds its own pages and the next window's first, a page sees the pages before it in its window, and memory stays bounded at any length. A window that stops early ends the document with its completed pages. --json lists one pages[] entry per page read: page (the source page), half on a split spread, run_index, window when windowed, text, layout (boxes in that page's own pixels), tokens and finish (marker, eos, page_cap, runaway, length, budget or cancelled), and raw under --raw. A v2_page_cap warning lists any page the per-page cap (--max-page-tokens, default 20,000) closed, which is almost always a runaway. FOCR_RUNAWAY_GUARD=1 (off by default here) closes such a page sooner: once a page passes 8,192 tokens and its last 2,048 repeat (any number counts as the same token, so a counting loop repeats too), the next page opens. The page ends runaway, keeps its text so far, and is listed in the same warning. With FOCR_REREAD_CAPPED_PAGES=1 (off by default), each such page is read again alone once the document is decoded. The re-read replaces the page when the model ends it, and a v2_page_reread warning lists the pages replaced and the pages kept. Robot page events are not sent again. Document mode composes with --split-spreads (each half is one page of the prompt) and with --extract-figures: figures are cropped from their page after the decode and named page{N}_figure_{M}, or page{N}l_figure_{M} / page{N}r_figure_{M} on a half. Robot page events add source_page, half, model_id, and for decoded pages tokens, finish, window (null in one pass) and, for a page read in one pass, elapsed_ms.

    Stopping and resuming a long document. After each window, a windowed Unlimited-OCR-2 run appends its pages to a journal and replaces a checkpoint: <out>.partial and <out>.focr-ckpt beside -o, or ~/.cache/franken_ocr/document-runs/<key>/ when the output goes to stdout. Ctrl+C or a spent stage budget writes the completed pages and exits 6 or 5, and the terminal run_error names the checkpoint. Re-running the same command (--resume says so explicitly) continues from the last completed window, and the result is byte-identical to an uninterrupted run; resumed_from on the terminal event gives the page decoding restarted at, and a stdout run prints the whole document again. The pages of a window that was interrupted are decoded again. A run whose input, page selection or decode settings differ is refused, with the fields named. --resume=recompute then keeps the completed pages and recomputes the next window: the pages after it can differ, so a resume_recompute warning says so. Both files are removed when the document finishes (--keep-checkpoint keeps the checkpoint), and focr doctor lists a stale stdout run with the command that continues it.

    Tall captures (full-page screenshots). A full-page browser capture (e.g. the Awesome Screenshot extension) is one extremely tall image — 1:4 to 1:10 aspect. A single model pass over such an image squashes glyphs below legibility and used to come back near-empty with exit 0. Two things now happen instead. First, single images with height ≥ 3× width are automatically OCR'd as smart-cut horizontal strips (each cut lands on the blankest row near the boundary so text lines are not severed) and the per-strip results are merged, with layout boxes mapped back to source-image coordinates — no flags needed; FOCR_TIMING=1 logs the strip plan. Second, a large input that still yields almost no text is flagged: robot mode adds low_yield: true (plus yield_chars and input_megapixels) to run_complete, human mode prints a stderr warning, and --fail-on-low-yield turns it into exit 8 (low_yield) before any output is written. A capture that is low-yield even after strip routing usually has glyphs below ~12px — re-capture at higher resolution; no OCR engine recovers 7px text. Manual alternative for exotic layouts: cut the page yourself and run focr ocr-batch strip1.png strip2.png ….

    Tasks (--task). Convenience routing over the model zoo (focr models). --task ocr (the default) is plain document OCR, unchanged. --task formula, tables, chart, molecular, and geometry are served by GOT-OCR2's OCR with format: mode, so each implies --format (an explicit --format composes idempotently) and needs the got-ocr2 model: focr pull got-ocr2, then --model got-ocr2.int8.focrq. --task chart and --task formula also pad the image to a white square first, because GOT misreads both when upstream's square resize stretches them; --format alone keeps that resize, and so does every other task. GOT can also read one region of an image, as upstream's fine-grained mode does: --box x1,y1,x2,y2 in the image's pixels (or a point x,y), or --color red|green|blue for a frame of that colour drawn around the text. Either composes with --format; a box does not compose with --task chart or --task formula, because they pad the image. --task music has two valid lanes: TrOMR is the native OMR path and returns MusicXML, while GOT-OCR2 can still run its sheet-music format mode. Use focr pull tromr, then --model tromr.int8.focrq --task music for a full printed/scanned page or a staff crop. Use focr pull tromr --quant f32 and --model tromr.focrq when you need the bit-exact reference artifact. You can also use the GOT lane (--model got-ocr2.int8.focrq --task music). --task describe (photo description / VQA) is served by the smolvlm2 model: focr pull smolvlm2, then --model smolvlm2.int8.focrq --task describe, optionally with --question "What color is the car?". SmolVLM2 has no instruction modes; the task is the question (default: the model-card caption prompt). --task chart-data is the OneChart path: it runs chart-to-dict extraction with the number-head reliability check and needs a onechart artifact, for example focr pull onechart, then --model onechart.int8.focrq. A reading that does not pass that check says so with a chart_unverified warning. Pointing a specialized task at the wrong model family fails with usage guidance before any weights load.

    Tuning flags: --base-size and --image-size (preprocessing resolution), --crop-mode (gundam or base), --max-length (decode token cap), --temperature (only 0: every model decodes greedily, and anything else exits 2 before a model loads), --no-repeat-ngram and --ngram-window (the sliding no-repeat n-gram decode guard).

    focr ocr-batch <images...>

    OCR many images in one process, loading the model once and reusing it across all pages.

    focr ocr-batch a.png b.png c.png --json     # one load, many pages
    FOCR_INT8_ATTN=1 FOCR_INT8_LMHEAD=1 \
      focr ocr-batch *.png --experimental-full-int8  # explicitly arm the gated cache
    FOCR_INT8_ATTN=1 FOCR_INT8_LMHEAD=1 FOCR_BATCH_SPINE=1 \
      focr ocr-batch *.png --experimental-full-int8 --json
    FOCR_BATCH_SPINE=0 focr ocr-batch *.png --json   # force the sequential oracle path
    

    With --json, each image's row says how its decode ended: finish_reason, any warnings, and chart on a OneChart run. An Unlimited-OCR-2 page stopped by its stage budget or position limit, or a v1 page the runaway guard stopped, keeps its markdown, but reads ok: false with an error, the same stop that makes focr ocr exit 5. So check ok, not whether markdown is present.

    ocr-batch loads the model once either way. Unlimited-OCR defaults to the conservative recipe path. Its continuous-batch R-SWA spine currently belongs to the explicitly gated all-int8 experiment, so FOCR_BATCH_SPINE=1 uses that spine only with --experimental-full-int8 and both accuracy switches shown above. The dense zoo path batches GOT-OCR2, SmolVLM2, and OneChart decode streams. GOT-OCR2 and OneChart reuse hydrated SAM weights, projectors, and widened embedding tables; SmolVLM2 does the same for its SigLIP tower, modality projection, and text embeddings. The same statics are cached on the loaded model for ordinary sequential pages too, so a multi-page PDF or repeated in-process calls no longer re-widen those large tensors for each page. FOCR_BATCH_SIZE defaults to 128 active streams and clamps at 256; FOCR_BATCH_PACK=1 groups similar prefill lengths before restoring output order. Without the experimental flag, attention and lm_head stay high precision by construction.

    To watch a long batch, add --progress-events PATH (- for stderr). Each line is one JSON object: batch_start with the resolved thread budget, model and backend, then image_complete or image_error as each image ends, with the pages per hour measured so far and eta_ms, and batch_end with the outcome. stdout does not change, so --json is still one final object. eta_ms is null until one image has ended, then the mean time per image so far times the images left; the first image carries the model load, so the estimate starts high. With --multi-page the lines are page_complete and page_skipped, and batch_start says the pages are one cross-page pass, not independent images. focr robot schema lists every field under batch_progress. The flag refuses FOCR_BATCH_SPINE, which decodes the images together and has no per-image end to report.

    --inputs-from FILE reads more paths, one per line, after any named on the command line (- reads stdin). --output-dir DIR writes each image's markdown to DIR/<stem>.md as it ends, so a stopped batch keeps what it finished; two inputs that would write one file are refused before any work.

    An image that fails does not stop the batch: the run goes on, and fails with the first failure's exit code only if no image was read. --fail-fast stops at the first failure instead. Ctrl+C stops at the image it interrupted, and the run exits 6. Either way the images that ended are still reported, and --json adds "stopped": "image_failed" or "stopped": "cancelled".

    focr pull

    Download a manifest-compatible model and tokenizer into the cache, verifying every byte. The embedded v0.7.0 Unlimited-OCR entry declares the runtime-compatible conservative recipe unlimited-ocr-ffn-int8-attn-bf16-lmhead-bf16-v1; the historical schema-v1 full-int8 entry remains incompatible and is rejected.

    focr pull                                   # versioned conservative Unlimited-OCR artifact
    focr pull unlimited-ocr-2                   # long-document successor (4.17 GB, three parts)
    focr pull got-ocr2                          # structured OCR model
    focr pull smolvlm2                          # image description / VQA model
    focr pull onechart                          # chart-to-data model
    focr pull tromr                             # sheet-music OMR model (int8 default)
    focr pull tromr --quant f32                 # bit-exact reference artifact
    focr pull got-ocr2 --quant int8 --json      # explicit quant, machine-readable
    focr pull --manifest ./manifest.json        # compatible custom default manifest
    

    Downloads run over asupersync's native HTTP stack (rustls + webpki-roots), reassemble GitHub split parts, verify against a committed SHA256 manifest, and cache to ~/.cache/franken_ocr/models. Manifest schema v2 requires an exact recipe for every quant; size, paths, counts, declared sizes, HTTPS URLs, and hashes are validated before installation. The release-bound schema-v2 manifest is embedded in each binary; a local or HTTPS override is explicit. The v0.7.0 primary Unlimited-OCR artifact uses the conservative exact recipe and installs as unlimited-ocr/unlimited-ocr.v0.7.0.int8.focrq. The embedded manifest pins its three release parts, byte count, and final SHA256; the historical full-int8 artifact remains only in the schema-v1 manifest used by already-released v0.6 binaries. Every model installs as one package under ~/.cache/franken_ocr/models/<model-id>/. Tokenizer sidecars cannot collide, and a pull replaces the whole directory at once, so a crash never leaves a new artifact beside an old tokenizer. The resolver searches those packages, then the cache root, so --model onechart.int8.focrq, --model tromr.int8.focrq, and --model tromr.focrq work after their pulls. TrOMR publishes int8 and f32; the default focr pull tromr fetches the int8 artifact, while focr pull tromr --quant f32 fetches the bit-exact f32 reference artifact.

    focr models

    List the model zoo with each id, runtime status, task set, model families, default artifact name, and compatible or blocked pull recipes.

    focr models                                 # human table
    focr models --json                          # machine-readable list
    

    Which model do I use?

    ModelStatusUse it forNotes
    unlimited-ocr (default)runtime ready, pull readyPlain-text document OCR for general documents and PDFs. This is what focr ocr runs by default.focr pull installs the v0.7.0 conservative exact-recipe artifact; raw BF16 and locally converted compatible artifacts are also supported.
    got-ocr2readySpecialized structured output the default cannot produce: formulas, tables, charts, molecular diagrams, geometry, and sheet music.Heavier per page; `--task formula
    smolvlm2readyPhoto description and VQA through --task describe [--question "..."].focr pull smolvlm2 installs smolvlm2.int8.focrq plus tokenizer.json in the model subdirectory.
    onechartreadyChart-to-data extraction through --task chart-data.focr pull onechart installs onechart.int8.focrq, vocab.json, merges.txt, and added_tokens.json. The native path runs the fixed chart prompt, SAM/projector splice, OPT KV-cache decode, num_decoder, JSON repair, and reliability distance check. Its number head is the part to trust; its text often is not, and an unverified reading warns.
    tromrreadyPolyphonic sheet-music OMR through --task music.focr pull tromr installs tromr.int8.focrq plus the rhythm, pitch, lift, and note tokenizer tables; focr pull tromr --quant f32 installs the bit-exact tromr.focrq reference artifact. Staff crops and full pages both run; bad staves are skipped with reasons when at least one staff recognizes, and over-budget systems can split at detected barlines.
    unlimited-ocr-2readyBaidu's long-document successor to the default model (sparse routed attention across pages).focr pull unlimited-ocr-2 installs the certified int8 artifact (4.17 GB) and its own tokenizer into models/unlimited-ocr-2/. --model unlimited-ocr-2 then reads single pages and documents of any length (--multi-page, read in windows of 10 pages); --model <dir> also runs a raw checkpoint. A document that stops early at its position limit or forward budget writes its pages, then exits 5. unlimited-ocr stays the default until v2 passes its default gates.
    trocr, pix2texplannedHandwriting and LaTeX OCR lanes.Registered descriptors only until their forward paths ship.

    unlimited-ocr is the fast default for ordinary text. Reach for got-ocr2 only when you need format extraction (formulas, tables, charts, etc.); it is a much larger decode and is not meant to replace the default for plain text. Install and run it with:

    focr pull got-ocr2                                    # download the weights + tokenizer
    focr ocr --model got-ocr2.int8.focrq page.png         # plain-text OCR
    focr ocr --model got-ocr2.int8.focrq --format eq.png  # structured .mmd: LaTeX / tables / charts / music
    focr ocr --model got-ocr2.int8.focrq --task tables page.png  # task shorthand (implies --format)
    

    How far to trust the structured lanes. These are small synthetic sets, measured on the published artifacts. They can show a lane failing; they do not show one working on your documents. None of these lanes warns on a wrong reading except OneChart.

    LaneMeasuredAdvice
    --task tablesOne table, read exactly.
    --task molecularOne molecule, read correctly.
    --task chart29 of 36 bar charts have every value right. The misses are plausible wrong numbers.Check the numbers against the chart.
    --task formula3 of 8 formulas right. The rest come back as SMILES or TikZ, not as bad LaTeX.Expect misses, and check each formula.
    --task music on GOT-OCR2One two-bar staff, misread.Use TrOMR.
    --task chart-dataText gave usable values on 0 of 36 charts. The number head averaged within 10% of the range on 31 of 36.Read chart.pred_locs from --json: bar heights from 0 to 1, in order. It does not say how many bars there are.

    Tables, the molecule, music and formulas read the same on the upstream f32 checkpoint. Charts do not, in both directions: at --task chart, int8 misses two charts that f32 reads right and reads three that f32 misses, so f32 would not fix this lane. Evidence: artifacts/accuracy/bd-kd75/ and tests/fixtures/got/format_corpus/.

    Run SmolVLM2 (photo description / VQA):

    focr pull smolvlm2
    focr ocr --model smolvlm2.int8.focrq --task describe photo.jpg
    focr ocr --model smolvlm2.int8.focrq --task describe --question "How many dogs are there?" photo.jpg
    

    Run OneChart (chart-to-data extraction):

    focr pull onechart
    focr ocr --model onechart.int8.focrq --task chart-data chart.png
    

    Run TrOMR sheet-music OMR:

    focr pull tromr
    focr ocr --model tromr.int8.focrq --task music score-page.png -o score.musicxml
    focr pull tromr --quant f32
    focr ocr --model tromr.focrq --task music score-page.png -o score.musicxml
    focr convert /path/to/tromr/model.safetensors -o tromr.int8.focrq --quant int8 --model-id tromr
    scripts/tromr_convert_e2e.sh
    scripts/tromr_music_e2e.sh
    scripts/realscan_music_gate.sh
    

    The TrOMR runtime accepts a single staff crop or a full printed/scanned page. If the detector finds multiple staves, it deskews the page globally, crops staves top-to-bottom, recognizes each staff through the same certified single-staff path, and emits one MusicXML part per staff; if it finds fewer than two staves, it treats the whole image as the staff input. For genuinely over-wide systems, the page path uses the five detected staff-line anchors to find thin barline columns, splits at the farthest usable barline within the positional budget, recognizes each segment sequentially, and concatenates the semantic stream while suppressing continuation clef/key/time artifacts. A multi-staff page succeeds when at least one staff recognizes, logging skipped staves with bbox and reason; an all-fail page errors with every staff reason named. The published int8 artifact is 61 MB and quantizes the 40 decoder-GEMM candidate tensors; the 86 MB f32 artifact stays published for bit-exact reference work. The int8 default is accepted with a ledgered discrepancy on one degraded Spohr page where both int8 and f32 garble diffe