Skip to main content

voce

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.

CI PyPI Python 3.9+ macOS and Linux MIT licence

Website · Install · Claude Code · Voices · Playlists

voce reading a Claude Code reply: the sentence being read is on a pale blue band, the word being spoken on a dark blue one, code is syntax highlighted and the table is lined up

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' === 1 is 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 -r reads 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. --export turns 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 to afplay.
  • 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

MIT

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)

Source distribution for voce 0.2.0
File Size Uploaded Provenance
voce-0.2.0.tar.gz 129.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for voce 0.2.0
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page