Read Markdown aloud in your terminal.
Each sentence lights up as it is read, and each word as it is spoken.
Code is read the way a pairing partner says it.
Website · Install · Claude Code · Voices · Playlists
voce (Italian for "voice", said VOH-chay) reads Markdown to you in the terminal. It shows the page as it should look, with headings, bold, links, highlighted code and lined-up tables, and reads it the way a person would: the prose as prose, and the code the way a pairing partner says it out loud. You can look away, follow along, or jump around with the keyboard or the mouse.
Coding agents write a lot worth knowing: explanations of a codebase, walkthroughs of a pull request, lessons on a language or a framework. voce lets you listen to it instead of reading it, away from the screen if you like. Ask Claude Code, Codex or any agent to write you something, and press play. It reads any Markdown.
- Reads code like a pairing partner.
'1' === 1is heard as "the string 1 is identical to 1", and$totals[$channel] ??= 0;as "totals channel null coalescing assignment zero". Operators get their manual's real names, in each language's own words, so you learn to say them. See Code, read aloud. - Reads what a person would read. Markup is never spoken. Tables are read row by row, and links by their text. Things meant for the eye stay on the screen.
- Follows along. The sentence being read sits on a pale band, and the word being spoken on a darker one. The page scrolls to keep your place, and a click on any sentence jumps there.
- Knows Claude Code.
voce -rreads Claude's latest reply in the folder you are in, with no copying. - Keeps your place. Quit at any point and it starts from the same sentence next time, even if the file has changed. It also reloads a file when it changes on disk.
- Sounds good, costs nothing. Kokoro, a small open voice, runs on your computer out of the box: no account, no key, nothing sent anywhere. Google's Chirp 3 HD, Azure, ElevenLabs and OpenAI voices are there when you set a key. Every sentence is fetched once and cached, so listening again is free and works offline.
- Goes with you.
--exportturns files into one MP3 with the same voice and pauses, for a walk or a drive.
Install
You need macOS or Linux, and Python 3.10 or newer for the voice that works out of the box.
uv tool install voce
# or
pipx install voce
Then try it:
voce README.md
Two optional extras make it better:
- The Xcode command line tools (
xcode-select --install) let voce build its small native audio player the first time it runs. You get gapless playback, a pause that keeps your place mid-word, and a word highlight that stays in time. Without them it falls back toafplay. - ffmpeg (
brew install ffmpeg) is needed for--export.
On Linux, voce plays audio with mpv or ffplay if one is installed, else with pw-play, paplay or aplay,
which play at one speed only. For the clipboard it uses wl-clipboard on Wayland, or xclip or xsel on X11:
sudo apt install mpv wl-clipboard # or: sudo dnf install mpv wl-clipboard
On Windows, run it in WSL, where it is Linux.
Use it
voce notes.md # read a file, from where you left off
voce a.md b.md # read several files in a row
voce -r # read Claude Code's latest reply in this folder
voce -c # read what is on the clipboard
cat notes.md | voce - # read standard input
voce # carry on with whatever you were last reading
voce --start notes.md # start again from the top
voce notes.md --export notes.mp3 # one MP3 to take with you
voce --check notes.md # how it will read: sentences, minutes, characters
Keys
| Key | Does |
|---|---|
| space | pause or carry on |
| ← → | previous or next sentence |
| ↑ ↓ | previous or next paragraph |
| [ ] | previous or next section |
| + - | faster or slower, from 0.8× to 2× |
| n p | next or previous file |
| v | next voice |
| V | next voice engine, of those set up |
| m | the playlist menu |
| r | hear this sentence again, or the file again at the end |
| c C | copy the sentence, or the whole paragraph |
| q | quit, keeping your place |
A click on a sentence or a line of code jumps to it. Drag across any text to copy it, without moving the voice:
it is on the clipboard when you let go. With --skip-code, a click on a code block copies it. To select text
with the terminal's own mouse instead, hold shift while you drag.
The mouse wheel scrolls, and the page follows the voice again at the next sentence. Vim users can use h j k l.
Code, read aloud
Every reader that speaks code gets it wrong in one of two ways. Screen readers read the symbols one by one:
"dollar, totals, left bracket, dollar, channel...". Text-to-speech apps read code as if it were prose, and Str::slug
comes out as "stress log". Neither says what a colleague sitting next to you would say.
voce reads code the way a pairing partner says it out loud, one line at a time, and the line sits on the band as it is read, with the word being spoken marked, just like a sentence of prose.
| On the line | What you hear |
|---|---|
$totals[$channel] ??= 0; |
totals channel null coalescing assignment zero |
usort($rows, fn ($a, $b) => $a['nights'] <=> $b['nights']); |
U sort rows, arrow function a, b returns a nights spaceship b nights |
foreach ($rows as &$row) { |
for each rows as by reference row |
'1' === 1; // false: a string is not an integer |
the string 1 is identical to 1, false: a string is not an integer |
if (strpos($s, 'Hi') !== false) { ... } |
if string position s, Hi is not identical to false and so on |
(int) (19.99 * 100); // 1998, a cent short |
cast to int 19.99 times 100, 1998, a cent short |
Str::slug('Sea View'); |
string double colon slug Sea View |
The rules are the ones a person uses without thinking. Names are said as words: array_values is "array
values", strlen is "string length", mb_strlen is "M B string length", usort is "U sort", isset is "is
set", checkIn is "check In". A variable's dollar sign is silent, and so are brackets and semicolons. Operators
get their real names: -> is "arrow", => is "double arrow", :: is "double colon", === is "is identical to"
and == is "is equal to", the PHP manual's own names rather than the "triple equals" nickname, ?? is "null coalescing", ?: is "short ternary", <=> is "spaceship", ?string is
"nullable string", (int) is "cast to int", ++ is "increment", & on a parameter is "by reference", and ... is
"unpack" in a call and "variadic" in a signature. The names are the PHP manual's own. A string
is read by its contents, so '' is "the empty string" and ',' is "a comma". A trailing comment is read as
prose. Inline code in a sentence is read the same way.
Because the names are the real ones, you learn them as you listen. After a few sessions, "identical", "null coalescing assignment" and "spaceship" come out of your mouth as easily as "equals", which is exactly what you want when you pair or review code with someone.
The rules were written for PHP and Laravel first, read right through laravel/framework with
docs/hearing/corpus.py to find what came out odd, and checked by ear against the real
voice with docs/hearing/check.py. A doc comment is read as a person reads it: @param array|string $columns is "parameter array or string, columns". Each language then has its own words on top of what is common
to all of them, in speech/words.py: a Python line says "returns" for ->, "floor
divided by" for // and "dunder init" for __init__; a JavaScript line says "is strictly equal to" for ===
and "optional chaining" for ?., which are MDN's names; shell says "pipe" and "and then"; SQL says "is not
equal to" for <>. A fenced block is read in the language on its fence, a diff in the language of its file,
and inline code in a sentence in the language most of the document's code is in. Prose inside code, such as a
doc comment or a heredoc, is read as prose. Adding a language is a few lines of data, and welcome.
If you would rather hear only the prose, --skip-code shows code without reading it.
Use it with Claude Code
Keep Claude Code in one terminal and open a second one in the same folder. When a reply lands, run:
voce -r
voce finds the session for that folder in ~/.claude/projects. It reads the same text that /copy would take,
which is Claude's final message without the progress notes, tool calls or thinking. If a turn is still running,
it reads the previous reply.
You can also /copy the reply and run voce -c, which works in any terminal and with any tool.
Voices
Kokoro works out of the box. It is a small open voice model that runs on your computer, so it is free, needs no account and sends nothing anywhere. The first time it speaks, voce downloads the model (350 MB) into its cache. It needs Python 3.10 or newer; on 3.9, voce uses the Mac's own voices instead (on Linux, install a newer Python).
Four cloud engines are used as soon as their key is set, and voce --voices shows which are:
| Engine | Set up with | Free each month, when this was written |
|---|---|---|
| Google Chirp 3 HD | VOCE_GOOGLE_API_KEY, or gcloud auth login and voce --google-project ID once |
1,000,000 characters, about 18 hours |
| Azure | VOCE_AZURE_KEY and VOCE_AZURE_REGION |
500,000 characters |
| ElevenLabs | VOCE_ELEVENLABS_API_KEY |
10,000 characters |
| OpenAI | VOCE_OPENAI_API_KEY |
nothing, but the cheapest per hour |
Google is the best value and is used first when more than one is set up. Azure never skips a word. ElevenLabs is the most expressive. OpenAI is told to read every word as written. voce counts the characters it sends to each, and the playlist menu shows the month's count against the free amount.
The Mac's voices are always there on macOS: voce --mac, or voce --mac --voice Daniel to pick one. Add
better ones in System Settings, under Accessibility, Spoken Content, System voice, Manage voices.
Press v to move through an engine's voices and V to move to the next engine; both are
remembered. Or name a voice and the engine follows: --voice bm_george (Kokoro), --voice en-GB-Chirp3-HD-Leda
(Google), --voice en-GB-RyanNeural (Azure), --voice ash (OpenAI), or --engine elevenlabs --voice Alice.
--engine picks an engine outright. If an engine fails part-way through, voce reads that sentence with Kokoro
and tells you why. The full guide, with Google's setup steps, is in docs/voices.md.
Playlists and quizzes
A playlist is a README with a ## Listen in this order section that lists Markdown files, grouped into parts:
## Listen in this order
### Part 1. The basics
1. [Getting started](https://github.com/jaystewartuk/voce/blob/main/basics/start.md) · 12 min
2. [Core ideas](https://github.com/jaystewartuk/voce/blob/main/basics/ideas.md) · 9 min
Run voce --playlist path/to/folder once. After that, voce works through the files in order, voce --menu lets
you choose one, and voce --list shows your progress. voce --prepare 1 fetches the audio for part 1 ahead of
time, and voce --minutes updates the "· N min" figures.
A file with a numbered list under ## Quiz and the same number of answers under ## Answers becomes a quiz. voce
reads a question, waits while you answer out loud, and reads the answer when you press space. voce --quiz runs
just the quizzes. See docs/playlists.md.
What it understands
Headings, paragraphs, bold, italic, struck-through text, inline code, links and images all work. So do nested, numbered and task lists, block quotes and rules. Fenced code blocks are highlighted for every language Pygments knows, and long lines wrap instead of being cut off. Tables line up in columns, respect column alignment, and are read one row at a time.
It also handles a few things that suit listening. Anything inside <details> is shown but not read. Lines that
are only links, such as breadcrumbs or "Next:" links, are shown but not read. Symbols such as →, ≈ and ×, and
abbreviations such as "e.g." and "i.e.", are read as words.
Code, in fenced blocks and in sentences, is read the way a pairing partner says it: see Code, read
aloud. A fenced diff block is highlighted with added lines in the file's syntax and removed
lines in red, and read hunk by hunk: "Line 40", then the changed lines, with "Added" and "Removed" said once at
the start of each run and the unchanged context shown but not read. Write the file's language after the word
diff, as in ```diff python, to have the added lines highlighted and read in that language.
Pick up on another device
voce --sync you/voce keeps your places, settings and a prepared copy of each document in a private git
repository of your own, so a second computer carries on where the first left off. A
web app reads the same repository on a phone or in any browser, with
a voice that runs in the page. There is no voce service: the repository is yours. See docs/sync.md.
Privacy
voce has no telemetry and makes no network calls of its own. With Kokoro or the Mac's voices nothing leaves your computer but the one-time download of the Kokoro model from GitHub. With a cloud engine, the text of each sentence goes to that engine's speech API and nowhere else. Everything else stays on your computer:
| What | Where |
|---|---|
| Your place in each document, one small file per document | ~/.local/state/voce/places/ |
| Speed, engine, voices and playlist | ~/.local/state/voce/settings.json |
| Documents prepared for other devices | ~/.local/state/voce/prepared/ |
| Characters sent to each engine this month | ~/.local/state/voce/usage.json |
| The last 50 clipboard, reply and piped texts | ~/.local/state/voce/clips/ |
| Cached audio and the compiled player | ~/.cache/voce/ |
voce respects XDG_STATE_HOME and XDG_CACHE_HOME. The cache is safe to delete at any time.
A place is named by a hash of the document's text, not its path, so the same file resumes wherever it is. With
--sync the state directory is a git repository; without it, VOCE_STATE can point it at a folder that iCloud
Drive, Dropbox or Syncthing carries between computers. Two machines reading different documents never touch the same
file, and the month's character counts stay on the machine that spent them.
How it works
voce parses Markdown into blocks of plain text, keeping bold, code and the rest as styled spans over that text.
The screen draws the spans, and the voice reads the plain text a sentence at a time. Audio is fetched up to eight
sentences ahead on a small thread pool. On macOS a tiny Swift program plays it over a pipe, gapless, and reports
its position every 50 milliseconds to time the word highlight; on Linux each sentence is handed to mpv or
ffplay. The interface is plain curses,
redrawn 25 times a second. docs/architecture.md has the details.
Contributing
Issues and pull requests are welcome, and so is a coding agent doing the work. The repository is set up for one: AGENTS.md tells Claude Code, Codex or Cursor how voce is laid out, how to test it and what to keep stable, so you can ask for the feature you want and review what comes back.
The easiest place to start is a new language for Code, read aloud: Ruby, Go, Rust, Java, C#
and Swift are all read with the common words today, and each needs only a few lines of data in
speech/words.py. CONTRIBUTING.md has the details.
Licence
Metadata
Release files for voce 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| voce-0.2.0.tar.gz | 129.8 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| voce-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 225.0 kB
Release files / voce-0.2.0.tar.gz
| Download URL | voce-0.2.0.tar.gz |
|---|---|
| Size | 129.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cc6c5958a80f3feaa46fc9a897334801827b9f1069fe1c9e180bde391daf0522
|
|
BLAKE2b-256 checksum How to use checksums |
5b0fac02071bc6a6697c955cfad5963eefa9caf7e70eebae21e4d9026f95294b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / voce-0.2.0-py3-none-any.whl
| Download URL | voce-0.2.0-py3-none-any.whl |
|---|---|
| Size | 95.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bc9870b3c70690a7f9dcb4bbbd9f7422156275c61157e2a24df54c337aa5e172
|
|
BLAKE2b-256 checksum How to use checksums |
546ccb559b8667b90dae0155c3335e1ce0861e62ad764a0c84f6bcb6e2ff7765
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for voce 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for voce-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.