╭────────────────────╮
│ │
│ ┃ │
│ ━━━━━━━┃━━━ │
│ ━━━━━━━┃ │
│ ━━━━━ ┃ │
│ ┃ │
│ │
╰────────────────────╯
Rhapsode
the page, read aloud
Rhapsode is a local booth. It reads an open web page or a saved document aloud with Kokoro, keeps your place, and can proofread the recording and bind it into an audiobook with chapters.
Open a saved page with rhapsode live, then choose the Chrome tab from the booth. A rhapsode stitched songs and spoke them. About tells that story and draws the desk.
Python 3.10 or newer, and ffmpeg on your PATH. The first time Rhapsode speaks, it downloads the Kokoro weights. A CUDA GPU is used when PyTorch can see one. Otherwise speech runs on the CPU.
python3 -m venv .venv
.venv/bin/pip install -U pip
.venv/bin/pip install -e ".[dev]"
ffmpeg -version
.venv/bin/rhapsode --versionOn Windows, create the environment with py -3 -m venv .venv and call .venv\Scripts\pip and .venv\Scripts\rhapsode.
[dev] adds pytest, ruff, mypy, and the wheel builder. For the program alone, use pip install -e ..
Open a page you already saved:
.venv/bin/rhapsode live page.jsonThe booth is at http://127.0.0.1:8765/. Choose tab needs Google Chrome left open, and Node.js, so the booth can ask Chrome which tabs are open. A document you already have does not need them. What the page does is in docs/desk-protocol.md.
This builds a file you can upload to PyPI. It does not upload it.
.venv/bin/python -m builddist/ then holds rhapsode-0.0.1-py3-none-any.whl and a .tar.gz. make wheel is the same command. The wheel includes the booth page, the icon, the page extractor, and the lexicon.
To try that file on this machine, without uploading it:
.venv/bin/pip install dist/rhapsode-0.0.1-py3-none-any.whl# Show the script without generating audio
rhapsode run my-page.json --dry
# Narrate a single document
rhapsode narrate my-page.json --voice af_bella --speed 1.2
# Full pipeline: narrate → proofread → encode with chapters
rhapsode run my-page.json
# Skip proofreading
rhapsode run my-page.json --no-proofread# Stage files into inbox/ for batch processing
rhapsode inbox page1.json page2.json
# Proofread a WAV produced by narrate
rhapsode proof output.wav timings.json my-page.json
# Encode WAV → M4B with chapter markers
rhapsode bind output.wav my-page.jsondocument.json ──build_script()──▶ utterances []
utterances ──Narrator──▶ WAV (lexicon applied per-utterance, speech cache: 200 MB default)
WAV + script ──proofread()──▶ ProofReport (WER + findings)
WAV + metadata ──encode()──▶ M4B/MP3 (with chapter markers)
| module | job |
|---|---|
document.py |
Parse JSON/Markdown/text into Document, split into Utterances |
narrate.py |
Kokoro-82M TTS — render each utterance, write timed WAV |
cache.py |
Speech cache — 200 MB SQLite + NumPy disk cache for Kokoro synthesis |
speech.py |
Text cleaning + pronunciation Lexicon (applied per-utterance before TTS) |
proof.py |
faster-whisper transcribe-back + word-level alignment |
bind.py |
ffmpeg M4B/MP3 encoding with FFmpeg metadata chapters |
paths.py |
Output/inbox/whisper-dir paths, WSL path interop, slugify |
cuda.py |
CUDA 12 library preloading |
load(path)— Parse a document (JSON from extractor, Markdown, or plain text)build_script(doc)— Split intoUtterances (text, kind, section, pause)Narrator().narrate(script, wav_path, Lexicon())— Speak each utterance, apply lexicon, write WAV with timings (cached viaSpeechCache)proofread(wav, script, timings)— Transcribe-back with whisper, compute WER and findingsencode(wav, out, ffmetadata(...chapters...))— Encode with chapter markers
kokoro # Kokoro-82M TTS. Brings in PyTorch
faster-whisper # transcription for proofreading
soundfile # audio I/O
numpy # audio arrays
num2words # number → word spelling
typer # the rhapsode command
mcp # Chrome tab reading, and the agent server
ffmpeg is a separate program. It is not installed by pip.
Documents are JSON with a top-level title and sections. Each section has a heading, level, and blocks:
{
"title": "My Page",
"url": "https://example.com/page",
"site": "example",
"lang": "en",
"sections": [
{
"heading": "Introduction",
"level": 1,
"blocks": [
{ "kind": "p", "text": "Full text of section one…" },
{ "kind": "heading", "text": "Subheading" }
]
}
]
}Block kinds: heading, p, li, quote, code, table.
extract.js (run via Chrome DevTools MCP / DevTools) produces this format from browser content.
Plain text and Markdown files are auto-converted on load:
rhapsode run article.md # Markdown headings become section boundaries
rhapsode run notes.txt # plain text → single paragraphVoice IDs are from the Kokoro-82M model — see hexgrad/Kokoro-82M. The first letter selects the language pipeline:
| prefix | language |
|---|---|
a |
American English |
b |
British English |
e |
Spanish |
f |
French |
h |
Hindi |
i |
Italian |
j |
Japanese |
p |
Portuguese |
z |
Mandarin |
Common voices: af_heart (American, default), bf_emma (British), ef_dora (Spanish), ff_siwis (French), if_alla (Italian), hf_english (Hindi).
Pronunciation overrides live in lexicon.txt within the package (built-in) and can be extended via ~/.config/rhapsode/lexicon.txt. Format:
# Rhapsode pronunciation lexicon
google = GOOGəl
x-ray = /[ks ray]/ # phoneme pin via misaki syntax
Lexicon is applied per-utterance before TTS rendering, so the output audio already reflects the substitutions.
Releases follow Semantic Versioning 2.0.0. The version in pyproject.toml is the only copy of the number. rhapsode --version prints it.
The first public revision is 0.0.1. Publish it by tagging that commit v0.0.1. The tag name is the version with a v prefix, and the two must match.
While the major version is 0, the public API is not stable. Patch numbers (0.0.2) are fixes. Minor numbers (0.1.0) may add or change behavior. 1.0.0 is the first stable API.
- Built with π: Grok 4.7 arbitrated π, the local coding agent. π ran Ollama
qwen3.6:27bwith a 65,536-token context window, on this machine's Blackwell GPU (NVIDIA GeForce RTX 5090, 32 GB of GPU RAM). The run used 64 GB of system RAM and 51 million edge tokens. - WSL2:
cuda.py.preload_cuda()handles CUDA 12 lib paths.output_dir()andinbox_dir()default to Windows-side paths (~/Music/Rhapsode,~/AppData/Local/Temp/rhapsode) for cross-OS access. SetRHAPSODE_OUT/RHAPSODE_INBOXto override. - Pause between utterances: auto-assigned per kind (
heading: 0.7s,p: 0.5s,li: 0.3s,quote: 0.6s,code: 0.6s,table: 0.35s). Extra 0.8s gap between sections. - Whisper models: Reuses models from
~/voxium/modelswhen present. SetRHAPSODE_WHISPER_DIRto override. - Output directory: Default
~/.cache/rhapsode/<slug>for work files,Music/Rhapsodefor final audio. SetRHAPSODE_OUTto customize.
