textrun
Generalized Wayland-compatible text reader with OCR and TTS. Captures screen content via screenshots, extracts text with OCR, and reads it aloud.
Repository: Codeberg (main) · GitHub (mirror)
Created in heavy cooperation with AI
Features
- OCR from screen capture any region or full screen, extract text
- Auto-scroll scrolls and re-captures until no new content appears
- Text stitching deduplicates overlapping OCR frames into clean text
- TTS output reads extracted text aloud via system TTS or command fallback
- Live reading text is spoken while still capturing (
tts.live), streaming or chunked - Configurable backends every step (screenshot, scroll, OCR, TTS, audio) is swappable
- Global shortcuts start and stop captures system-wide while the GUI or
--shortcutsdaemon runs (capture_shortcut,capture_shortcut_stop; Wayland global-shortcuts portal or pynput on X11/Win/Mac) - Mixed-DPI aware pixel-accurate captures across monitors with different scales
- Chain fallback if a backend fails, the next in chain is tried automatically
- Profiles per-app settings (Firefox, games, terminals) with inheritance from global
- GUI PySide6 config manager with region picker, profile editor, backend reorder
- Wayland support dotool/ydotool for scroll, grim for screenshots, speech-dispatcher for TTS
Installation
Recommended: uv
uv is the fastest way to install Python tools.
# Install textrun for your platform
uv tool install 'textrun[wayland]' # Wayland (Sway, Hyprland, GNOME, KDE)
uv tool install 'textrun[x11]' # X11
uv tool install 'textrun[mac-win]' # Windows / macOS
The GUI (PySide6) is a core dependency and always included.
From a local clone
Instead of PyPI you can install straight from a checkout:
git clone https://codeberg.org/marvin1099/textrun
cd textrun
# Same as above, but pointing at the cloned repo (.):
uv tool install --with 'textrun[wayland]' .
uv tool install --with 'textrun[x11]' .
uv tool install --with 'textrun[mac-win]' .
Alternative: pipx
pipx installs Python CLI tools in isolated environments.
# Install textrun for your platform
pipx install "textrun[wayland]"
pipx install "textrun[x11]"
pipx install "textrun[mac-win]"
Last resort: pip
pip install --user "textrun[wayland]"
pip install --user "textrun[x11]"
pip install --user "textrun[mac-win]"
Make sure ~/.local/bin is in your PATH.
Usage
# Basic usage (global profile)
textrun
# Use a specific profile
textrun -p wayland
textrun -p firefox
textrun -p game-terminal
# Capture a specific region
textrun -p wayland --region 100,200,800,600
# Save text to file
textrun -p firefox -o output.txt
# Disable TTS
textrun --no-tts
# Override settings
textrun --scroll-amount 5 --max-scrolls 100
# List available profiles
textrun --list-profiles
# Launch the GUI
textrun --gui
textrun -p wayland --gui
# Daemon-like: wait for global shortcuts only, no window
textrun --shortcuts
# Verbose logging
textrun -v -p wayland
Desktop entry
textrun --install-desktop installs a menu entry, so textrun can also be
used from the app launcher without a terminal: launching it opens the GUI,
and a secondary "Shortcuts daemon" action starts --shortcuts.
Configuration
Config file: ~/.config/textrun/config.toml
Backend types
Every backend in the config has a type field:
| Type | Description | Example |
|---|---|---|
command |
Runs an external program via subprocess | grim, dotool, piper |
library |
Calls a Python library function directly | pytesseract, rapidocr |
adapter |
Calls a textrun adapter for stateful libraries | speechd, pyttsx3 |
Config example
# Global defaults (inherited by all profiles)
[global]
scroll_amount = 3
scroll_delay = 1.0
max_scroll_attempts = 50
language = "eng"
tts_enabled = true
tts_volume = 100 # percent; {tts_volume}/{tts_volume_pa} placeholders
# Screenshot backend chain
[global.screenshot]
chain = ["pyscreenshot", "grim", "spectacle"]
[global.screenshot.backends.grim]
type = "command"
program = "grim"
args = ["{output_path}"]
# Scroll backend chain
[global.scroll]
chain = ["pyautogui", "xdotool", "dotool", "ydotool"]
[global.scroll.backends.dotool]
type = "command"
program = "dotoolc"
args = []
stdin = "wheel -{scroll_amount}"
# OCR backend chain
[global.ocr]
chain = ["tesseract", "rapidocr"]
[global.ocr.backends.tesseract]
type = "library"
library = "pytesseract"
function = "image_to_string"
args = { image = "{image_path}", lang = "{language}" }
# TTS backend chain
[global.tts]
chain = ["speechd", "pyttsx3", "piper", "espeak-ng"]
[global.tts.backends.speechd]
type = "adapter"
adapter = "textrun.adapters"
function = "speechd_say"
Placeholders
Placeholders like {key} are resolved from the runtime context:
| Placeholder | Source |
|---|---|
{scroll_amount} |
config scroll_amount |
{output_path} |
generated per run |
{image_path} |
generated per run |
{text} |
OCR output |
{audio_path} |
generated per run |
{language} |
config language |
{region_str} |
formatted region |
Commands
Command backends support these fields:
[backends.my-command]
type = "command"
program = "dotoolc" # binary name, resolved via PATH
args = ["arg1", "{var}"] # list, placeholders resolved
stdin = "text {var}" # string template: piped to stdin
env = { KEY = "val" } # extra environment variables
stdout = true # capture stdout
stderr = true # capture stderr
stdin accepts a string template (recommended, e.g. "{text}" for TTS) or true
(pipes the internal stdin_text context key). Stream backends (type = "stream")
are fed incrementally by the live reader and don't use stdin.
Profiles
Profiles inherit from global by default. Override only what differs.
Built-in profiles
| Profile | Description |
|---|---|
global |
Base defaults for all settings |
wayland |
dotool scroll, grim screenshots |
firefox |
Larger scroll amount, slower delay |
game-terminal |
Arrow key scroll, no TTS, fast timing |
Profile example
[profiles.firefox]
inherits = "global"
scroll_amount = 5
scroll_delay = 1.5
[profiles.firefox.scroll]
chain = ["dotool"]
Region in profiles
Set a capture region per profile:
[profiles.firefox]
inherits = "global"
scroll_amount = 5
[profiles.firefox.region]
x = 100
y = 200
width = 800
height = 600
The GUI also saves two companion keys for pixel-accurate captures:
[profiles.firefox.screen_size]
width = 2560
height = 1440
[[profiles.firefox.monitors]]
x = 0
y = 0
width = 2560
height = 1440
scale = 1.5
region uses logical desktop coordinates; monitors records each
monitor's layout and scale so crops stay correct on mixed-DPI setups.
Both are written by the GUI region picker, set them by hand only if you
must.
GUI
Ships with PySide6 as a core dependency, no extra install needed.
textrun --gui
Features:
- Profile selector switch between profiles
- Region picker click and drag to select capture area
- Backend reorder drag and drop to change fallback order
- Start/Stop buttons + global shortcuts capture without touching the CLI
- Copy command copies
textrun -p <profile>to clipboard - Save config writes settings to
~/.config/textrun/config.toml
Headless variant: textrun --shortcuts registers the global shortcuts
(capture_shortcut, capture_shortcut_stop) and waits without showing a
window; captures run with the selected profile's saved config.
Tip: run textrun --install-desktop once. The Wayland global-shortcuts
portal attributes shortcuts to the launching app, started from a
terminal they appear under e.g. "Konsole"; with the desktop entry
installed and textrun launched from the app menu, they register under
"textrun" with its own icon.
Backend defaults
Screenshot
| Order | Backend | Type | Notes |
|---|---|---|---|
| 1 | pyscreenshot | adapter | Auto-detects Wayland/X11 |
| 2 | grim | command | Wayland native (wlroots) |
| 3 | spectacle | command | KDE native |
Scroll
| Order | Backend | Type | Notes |
|---|---|---|---|
| 1 | pyautogui | adapter | X11, may prompt on Wayland |
| 2 | xdotool | command | X11 only |
| 3 | dotool | command | Wayland, needs /dev/uinput |
| 4 | ydotool | command | Wayland, needs ydotoold |
Wayland profile order: dotool → ydotool → pyautogui → xdotool
OCR
| Order | Backend | Type | Notes |
|---|---|---|---|
| 1 | tesseract | library | Fast, good for clean text |
| 2 | rapidocr | library | Better accuracy, heavier |
TTS
| Order | Backend | Type | Notes |
|---|---|---|---|
| 1 | speechd | adapter | Linux system standard |
| 2 | pyttsx3 | adapter | Cross-platform (Win/Mac/Linux) |
| 3 | piper | command | Neural quality, needs model |
| 4 | espeak-ng | command | Robotic, always available |
Audio playback
| Order | Backend | Type | Notes |
|---|---|---|---|
| 1 | playsound3 | adapter | Cross-platform via GStreamer (no volume support) |
| 2 | mpv | command | Plays anything, honors tts_volume |
| 3 | ffplay | command | Fallback, honors tts_volume |
Volume
tts_volume (percent, default 100) is set in [global] and inherited by
profiles. It reaches backends as template placeholders:
{tts_volume}percent, for ffplay (-volume) and mpv (--volume=){tts_volume_pa}linear 0–65536, for paplay (--volume=) and pactl
The piper-stream player and the mpv/ffplay audio backends use these by default. Native TTS engines (speechd, pyttsx3) and playsound3 ignore it.
Live reading
With tts.live = true in [global.tts], text starts playing while
capture is still running instead of after the last frame:
[global.tts]
live = true # read incrementally during capture
live_queue_chunks = true # see below
- Streaming TTS (
piper-stream): synthesis and playback run as a continuous pipeline words come out as lines are confirmed stable. - Chunked TTS (all other backends): confirmed text is queued and
spoken chunk-by-chunk;
live_queue_chunkscontrols whether chunks are pre-synthesized ahead of playback.
The stitcher holds back the last two lines until later frames confirm them; when capture ends they are flushed and read too. The stop shortcut cuts off reading immediately. If live playback never produced audio, textrun falls back to one batch read at the end.
Optional dependencies
Platform bundles include everything needed for that platform:
uv pip install "textrun[wayland]" # OCR + screenshots for Wayland
uv pip install "textrun[x11]" # OCR + pyautogui for X11
uv pip install "textrun[mac-win]" # OCR + pyttsx3 for Windows/macOS
uv pip install "textrun[all]" # Everything
Or install individual extras:
uv pip install "textrun[ocr-tesseract]" # Tesseract OCR
uv pip install "textrun[ocr-rapid]" # RapidOCR
uv pip install "textrun[scroll-pyautogui]" # pyautogui scroll
uv pip install "textrun[tts-pyttsx3]" # pyttsx3 TTS
uv pip install "textrun[screenshots-pyscreenshot]" # pyscreenshot
PySide6 (GUI), Pillow, imagehash and playsound3 are core dependencies and always installed.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file textrun-0.1.1.tar.gz.
File metadata
- Download URL: textrun-0.1.1.tar.gz
- Upload date:
- Size: 72.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b0a4434ff2a9483fe3a0315e4c59731449a005f0038eeb2c47e4d35a0e442a6
|
|
| MD5 |
382fbd7284bec8b14e43fcb34d3e7a28
|
|
| BLAKE2b-256 |
da9e5a8fd32af5424ff55600066b7474702193a10bd1fd0566ddbe7bc047f658
|
File details
Details for the file textrun-0.1.1-py3-none-any.whl.
File metadata
- Download URL: textrun-0.1.1-py3-none-any.whl
- Upload date:
- Size: 65.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
caa28aa260176c073c9cadb0699130e429fdeca5f7d74da7c2cd308358a1009f
|
|
| MD5 |
b942c269b57107252a9a64620e63fb87
|
|
| BLAKE2b-256 |
aae739aec8d5dff6cbdb8ce6b47e86f8db48b44d060b6bba75ef4373bef383df
|