Snap any image, screenshot, or webpage into plaintext. No GPU. No cloud. One command.
textsnap screenshot.png
That's it. You get a .txt next to your shell, recognized on your CPU, from a screenshot, a photo, an image URL, or even a webpage.
- ⚡ Runs on CPU. A 0.9B PaddleOCR-VL-1.5 vision-language model, quantized to q4 ONNX, parses full pages on a plain laptop. No CUDA. No M-series-only tricks. Plain old cores, pinned to your physical-core count.
- 🖼 Images, screenshots, URLs, webpages. Point it at a local file, a direct image URL, or a full article URL — it isolates the main content and OCRs the most prominent image. Or OCR straight from your clipboard with no argument at all.
- 📴 Offline after first run. ~890 MB of ONNX downloads once to your cache and stays there. No API keys. No quotas. Your images never leave your machine.
- 🪶 One file. The whole tool is a single Python module. Dependencies install themselves on first run if missing.
- 📝 Markdown or plaintext. Default output is the model's native markdown (tables, headings, structure preserved). Add
--plaintextto flatten it.
# Install
pip install textsnap
# Snap something
textsnap screenshot.png
textsnap https://example.com/article --plaintext
textsnap photo.jpg -o ~/notes/receipt.txt
The first run downloads the model (~890 MB). Every run after is offline.
| Source | Example |
|---|---|
| Clipboard | textsnap (no argument) |
| Local image file | textsnap path/to/img.png |
| Direct image URL | textsnap https://example.com/x.png |
| Webpage URL | textsnap https://example.com/article |
Local files cover anything Pillow can decode: .png, .jpg, .jpeg, .webp, .bmp, .gif, .tiff, and friends. For webpage URLs, textsnap uses readability to isolate the main content, then picks the most prominent image on the page and OCRs that.
pip install textsnap
Installs two equivalent commands on your PATH: textsnap (canonical) and ocr (alias, for when the name slips your mind).
To install from a local source checkout instead:
pip install .
For a reproducible install with exact pinned dependency versions:
pip install -r requirements-lock.txt
pip install .
Clipboard note. Reading images from the clipboard relies on Pillow's
ImageGrab. On Linux you may needxcliporwl-clipboardinstalled. macOS and Windows work out of the box.
# Clipboard (no argument)
textsnap
# Local image file
textsnap path/to/screenshot.png
# Direct image URL
textsnap "https://example.com/diagram.png"
# Webpage — OCRs the most prominent image on the page
textsnap "https://example.com/article"
# Flatten the model's markdown to plain text
textsnap input.png --plaintext
# Custom output path
textsnap input.png -o ./out/extracted.txt
# Raise the token cap for very dense pages
textsnap dense-page.png --max-tokens 4096
# Use a local model directory instead of downloading
textsnap input.png --model-dir ~/models/paddleocr-vl
Plaintext, UTF-8. Default location is ./textsnaps/ (created if missing) under the current working directory; override with -o. The filename is derived from the image filename stem (receipt_ocr.txt), or from the webpage slug for URL inputs.
textsnap is quiet by default, Unix-style: the only thing printed to stdout is the path to the file it wrote, so it composes cleanly —
OUT=$(textsnap receipt.png) # capture the path
textsnap receipt.png | xargs cat # print the recognized text
Pass -v to send progress diagnostics (input type, image size, decode speed, token counts) to stderr; stdout stays just the path either way.
Default file output is the model's native markdown — it preserves tables, headings, and document structure:
# Quarterly Report
| Region | Revenue |
| ------ | ------- |
| EMEA | $1.2M |
| APAC | $0.9M |
With --plaintext, markdown is flattened to bare text:
Quarterly Report
Region Revenue
EMEA $1.2M
APAC $0.9M
| Flag | Description |
|---|---|
-o, --output |
Output .txt path. Default: ./textsnaps/<name>_ocr.txt. |
-v, --verbose |
Print progress diagnostics to stderr. Off by default. |
--plaintext |
Flatten the model's native markdown to plain text. |
--model-dir |
Use ONNX/config files from this directory instead of downloading. |
--max-tokens |
Cap generated tokens. Default 2048. Raise it for very dense pages. |
--no-verify |
Skip SHA-256 verification of downloaded model files (not advised). |
--generate-checksums |
Download the pinned model files, write a fresh manifest, and exit. |
textsnap auto-downloads ~890 MB of model weights from the Hugging Face Hub on first run, so it treats those files as untrusted until proven otherwise:
- Pinned model revision. Downloads are pinned to a specific repo revision, so a moved or retagged
maincan't silently swap the weights. - SHA-256 verification. Every downloaded file is hashed and checked against known-good digests before it's loaded. A mismatch aborts the run with a clear error rather than executing unverified weights. Digests live in
model_checksums.sha256and are also embedded in the script as a fallback, so verification works whether you install from source or from a wheel. - Pinned dependencies.
requirements-lock.txtpins exact dependency versions for reproducible installs; the file documents how to add per-wheel--hashentries withpip-compile --generate-hashesfor full supply-chain pinning.
Regenerate the checksum manifest after a deliberate model-revision bump:
textsnap --generate-checksums
To bypass verification (for local experimentation with a modified model), pass --no-verify.
- Load. From the clipboard, a local file, a direct image URL, or — for a webpage URL — the most prominent image inside the page's main content (readability + a prominence heuristic).
- Preprocess. The image is capped at 640px on its longest side, then run through PaddleOCR-VL's Qwen2-VL-style smart-resize and patchify, producing the pixel-value tensor and grid the vision encoder expects.
- Recognize. Three ONNX components run on CPU: a vision encoder (q4), a token-embedding model (fp32), and an autoregressive decoder (q4) with a wired-up KV cache. Greedy decode, with a repetition guard that stops runaway loops early.
- Format. Native markdown by default;
--plaintextreduces it to bare text.
No image is sent anywhere. No state is kept between runs except the cached model.
The PaddleOCR-VL-1.5 ONNX components are downloaded on first run to ~/.cache/textsnap/:
onnx/vision_encoder_q4.onnx— vision encoder + spatial-merge projectoronnx/decoder_q4.onnx— autoregressive decoderonnx/embedding.onnx— token embeddings (fp32; no q4 variant exists)tokenizer.json,config.json
Together ~890 MB. To use your own copy, point --model-dir at a directory containing the same onnx/ files plus tokenizer.json and config.json.
- First run is the slow one — it downloads ~890 MB. After that, textsnap is fully offline.
- CPU decode is sequential. Dense, full-page documents take longer than a short screenshot. textsnap pins thread counts to your physical cores and prints a live tokens/sec readout so a slow run is visibly alive, not hung.
--max-tokenscaps the output. Very dense pages can hit the default 2048-token cap and truncate; raise it if the tail of a page is missing.- Webpage inputs OCR one image — the most prominent one in the main content, not the whole rendered page.
- Greedy decoding can occasionally loop on repetitive layouts; a built-in guard detects and trims these.
MIT for this project — see LICENSE.
The model is PaddleOCR-VL-1.5, distributed under Apache-2.0 by PaddlePaddle; textsnap pulls the ONNX export from onnx-community/PaddleOCR-VL-1.5-ONNX. See the original model card for model terms. Powered by onnxruntime and huggingface_hub.
