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

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.
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_ocr/main/install.sh | bash
focr pull仅需 CPU,无需 GPU 或 CUDA;支持 macOS Apple Silicon/Intel、Linux x86-64/ARM64、Windows x86-64/ARM64;Linux 需 glibc 2.17 以上。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 等内核
容易踩的坑
- v0.10.0最新正式版2026-11-09 · 2026-11-09 13:15 UTC
v0.10.0is a feature release. It ships the same six-platform raw-executable matrix asv0.9.x(each asset with a SHA256 sidecar) and still pins the hash-verifiedv0.7.0Unlimited-OCR model artifact. Install with the one-liner;focr pullfetches 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_THREADSstill override it. --temperatureabove 0 is refused before a model loads (bead bd-d630). Every model decodes greedily, so atemperature > 0request used to load the weights and then exit 1; it now fails fast with exit 2 and says why.--temperature 0is 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: runawayand exits 5 (ocr-batchmarks the image failed and continues).FOCR_RUNAWAY_GUARD=0restores 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 doctornow 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
stageevents (load/forward, with elapsed and budget), and the resident daemon now serves--task musicand--model unlimited-ocr-2runs 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: asupersync7a0f2f55, frankentorch4b23eb8a, frankensqlite2f401ee7. - 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.exematch on machine and subsystem, imported-DLL list, and C-runtime model (the dynamicvcruntime140.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.
- 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
- models-unlimited-ocr-2-v1Unlimited-OCR-2 int8 weights (models-unlimited-ocr-2-v1)2026-11-02 · 2026-11-02 17:38 UTC
查看更新说明
int8
.focrqweights for franken_ocr's second model, Baidu Unlimited-OCR-2 (MIT). Recipeunlimited-ocr-2-ffn-int8-attn-bf16-lmhead-bf16-v1, converted from HFbaidu/Unlimited-OCR-2at commit 5632329a.The reassembled
unlimited-ocr-2.v0.1.0.int8.focrqis 4,173,206,653 B, sha25654b8a1ecaf0c1311eab5bb16cf224bca67772056e1d713d5538b5486035b4398. GitHub caps an asset at 2 GiB, so it ships as three parts.focr pull unlimited-ocr-2reassembles 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, sha2569e940a5891b6abcbfc87594c3dc943ed80cd835083d332758f62763606e0b227unlimited-ocr-2.v0.1.0.int8.focrq.part01: 1,391,068,885 B, sha256111efd9e7f7b6fbdf1efbf7101438bf79c4222aacaef18f1a9c9a2bf92ba3cd6unlimited-ocr-2.v0.1.0.int8.focrq.part02: 1,391,068,883 B, sha25634bd2eba13ce4afe75a90fd21710149476d0cc0f89c285449cd22b4466aeca02tokenizer.json: 9,980,278 B, sha256c55c165e32c5a67830362a53e1f548c8930dcc8469efa30ff3252a982799c589. It is the v2 tokenizer, byte-identical to the one atbaidu/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.
- v0.9.1v0.9.1 — --multi-page keeps text outside <PAGE> markers2026-09-29 · 2026-09-29 02:55 UTC
查看更新说明
v0.9.1is a patch release for--multi-pageoutput. It ships the same six-platform raw-executable matrix asv0.9.0(each asset with a SHA256 sidecar) and still pins the hash-verifiedv0.7.0Unlimited-OCR model artifact. Install with the one-liner;focr pullfetches weights.Fixed
--multi-pagekept only the<PAGE>markers and dropped the text (GH #17). Page assembly followed the reference implementation'ssplit('<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: asupersync78b64636, frankentorch4b23eb8a, frankensqlite2f401ee7.
Verification
scripts/check.shvalidators and installer end-to-end test,cargo fmt --check,cargo check --locked --all-targets, andcargo clippy --locked --all-targets -- -D warningsgreen;cargo test --lockedpassed 1,436 tests across 32 suites with no failures.tests/pinned_fixture_hashes.rsnow 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.
- 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.0ships the same six-platform raw-executable matrix asv0.8.0(each asset with a SHA256 sidecar) and continues to pin the immutable, hash-verifiedv0.7.0Unlimited-OCR model artifact (unlimited-ocr-ffn-int8-attn-bf16-lmhead-bf16-v1). Install with the one-liner;focr pullfetches 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_completerobot event carrieslow_yieldfields and a stderr warning is printed. Opt-in--fail-on-low-yieldturns 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, repairingcargo installbuilds 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|backendsJSON-validated,robot selftestint8 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: asupersync76c93637, frankentorch9627f39c, frankensqlitebda32118.
Verification
cargo fmt --check,cargo check --locked --all-targets, andcargo clippy --locked --all-targets -- -D warningsgreen at the release commit; the fullcargo test --lockedsuite green on the Linux build host (31 suites, 0 failures).- Release-ref gate:
scripts/gauntlet_cert.py --dist-ref-preflightPASS for tagv0.9.0at1f7f669(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 repairedbda3211before 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.
- 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.0ships the same six-platform raw-executable matrix asv0.7.2(each asset with a SHA256 sidecar) and continues to pin the immutable, hash-verifiedv0.7.0Unlimited-OCR model artifact (unlimited-ocr-ffn-int8-attn-bf16-lmhead-bf16-v1). Install with the one-liner;focr pullfetches weights.Highlights
- Resident warm-model daemon (GH #9).
focr ocrnow 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-residentorFOCR_NO_RESIDENT=1. - Installer repair (GH #12).
install.sh/install.ps1no longer trustGET /releases/latest(which broke every fresh install once amodels-*weights release became the newest release): both now enumerate/releasesand select the newest semverv*tag. - Machine-parseable
--version(GH #13). stdout carries onlyfocr <semver>; the license attribution lines moved to stderr. - Unlimited timeout escape hatch (GH #10).
FOCR_STAGE_BUDGET_FORWARD_MS=0(orunlimited) disables the forward stage budget instead of being silently ignored. - Agent-facing robot upgrades.
run_errorevents carry a machine-consumablerecoveryfield;focr robot schemaself-describes the steering environment variables, anagent_discoverycommand map, and the stdout contract; root--helppoints agents atfocr 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.2it 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: asupersync73e00b34, frankentorch14ae04bc, frankensqlite065cafc3.
Verification
cargo fmt --check,cargo check --locked --all-targets,cargo clippy --locked --all-targets -- -D warnings, and the fullcargo test --lockedsuite 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), andfocr robot selftest(int8 kernels bit-identical to the scalar oracle) on its build host before staging; Linux assets passed thereadelfglibc-2.17 floor check.
Full history: see CHANGELOG.md §0.8.0.
- Resident warm-model daemon (GH #9).
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
franken_ocr
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?
| Feature | What it does |
|---|---|
| One portable binary | No 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 offline | Once 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 API | OcrEngine exposes synchronous, blocking calls for Markdown, structured layout, figure extraction, in-memory images, and load-once batches. |
| Native PDFs and figures | Scanned 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 status | Ready 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 path | Historical 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 dispatch | One 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 gauntlet | docs/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 path | focr 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 instrumentation | FOCR_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 history | focr 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 memory | R-SWA keeps generated-token KV constant (window 128) while the reference block is held as a frozen, never-evicted global KV. |
| Agent-first | Versioned 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 kernels | focr 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 gate | The 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 evidence | scripts/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
| Layer | What is live today |
|---|---|
| Inputs | PNG/JPG-style images, scanned PDFs, selected PDF page ranges, split book spreads, image batches, full sheet-music pages, and single staff crops. |
| Model routing | Unlimited-OCR for general documents, GOT-OCR2 for structured OCR modes, SmolVLM2 for describe/VQA, OneChart for chart data, and TrOMR for MusicXML. |
| Runtime core | OcrEngine owns one asupersync runtime, caches one model per process, exposes blocking Rust calls, and records best-effort run telemetry through fsqlite. |
| CPU execution | Fixed-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. |
| Outputs | Markdown, 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.0 | Official Unlimited-OCR | llama.cpp | ONNX Runtime | |
|---|---|---|---|---|
| Language / runtime | Pure Rust, one binary | Python + HF transformers | C++ | C++ |
| Primary target | CPU | CUDA GPU | CPU/GPU | CPU/GPU |
| Scope | A few hand-tuned models | This model | Many models | Many models |
| int8 kernels | Model-specific tiled SDOT/SMMLA/VNNI | n/a | Generic K-quant | MLAS |
| Vision encoder | First-class, kept high precision | First-class | Kept F16 (mmproj) | Depends on export |
| Network at inference | None | None | None | None |
| Ships as | Single static binary, no FFI | Python env + CUDA | Binary + model | Library + model |
| Constant generated-token KV | Yes (R-SWA preserved) | Yes | Depends on PR support | Depends |
| Runs with no GPU | Yes | No (needs CUDA) | Yes | Yes |
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_ocrruns 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.
| Platform | Asset |
|---|---|
| macOS Apple Silicon | focr-aarch64-apple-darwin-neon-sdot-i8mm |
| macOS Intel | focr-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-64 | focr-x86_64-pc-windows-msvc.exe |
| Windows ARM64 | focr-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
- Install the binary with the curl one-liner, or download and verify a release asset by hand.
- Install compatible weights: run
focr pullfor the versioned conservative Unlimited-OCR artifact. Long documents can usefocr pull unlimited-ocr-2; specialized models usefocr pull got-ocr2,focr pull smolvlm2,focr pull onechart, orfocr pull tromr. Raw BF16 and locally converted exact-recipe artifacts remain available throughFOCR_MODEL_PATH. - 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. - OCR a page:
focr ocr page.pngfor Markdown, add--jsonfor structured output (markdown + bounding boxes),-o out.md/-o out.jsonto write a file, or--robotfor an NDJSON event stream. - Batch many pages in one process (model loaded once):
focr ocr-batch page1.png page2.png page3.png --json. Add--multi-pageto 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:
| Method | Use 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
 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?
| Model | Status | Use it for | Notes |
|---|---|---|---|
unlimited-ocr (default) | runtime ready, pull ready | Plain-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-ocr2 | ready | Specialized structured output the default cannot produce: formulas, tables, charts, molecular diagrams, geometry, and sheet music. | Heavier per page; `--task formula |
smolvlm2 | ready | Photo description and VQA through --task describe [--question "..."]. | focr pull smolvlm2 installs smolvlm2.int8.focrq plus tokenizer.json in the model subdirectory. |
onechart | ready | Chart-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. |
tromr | ready | Polyphonic 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-2 | ready | Baidu'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, pix2tex | planned | Handwriting 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.
| Lane | Measured | Advice |
|---|---|---|
--task tables | One table, read exactly. | |
--task molecular | One molecule, read correctly. | |
--task chart | 29 of 36 bar charts have every value right. The misses are plausible wrong numbers. | Check the numbers against the chart. |
--task formula | 3 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-OCR2 | One two-bar staff, misread. | Use TrOMR. |
--task chart-data | Text 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