voce 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. voce-0.2.0/.gitignore +13 -0
  2. voce-0.2.0/AGENTS.md +80 -0
  3. voce-0.2.0/CHANGELOG.md +150 -0
  4. voce-0.2.0/CLAUDE.md +1 -0
  5. voce-0.2.0/CONTRIBUTING.md +93 -0
  6. voce-0.2.0/LICENSE +21 -0
  7. voce-0.2.0/PKG-INFO +332 -0
  8. voce-0.2.0/README.md +303 -0
  9. voce-0.2.0/SECURITY.md +11 -0
  10. voce-0.2.0/docs/architecture.md +195 -0
  11. voce-0.2.0/docs/brand.md +44 -0
  12. voce-0.2.0/docs/examples/laravel.md +229 -0
  13. voce-0.2.0/docs/google-voices.md +3 -0
  14. voce-0.2.0/docs/hearing/check.py +117 -0
  15. voce-0.2.0/docs/hearing/corpora.txt +2 -0
  16. voce-0.2.0/docs/hearing/corpus.py +211 -0
  17. voce-0.2.0/docs/hearing/phrases.txt +51 -0
  18. voce-0.2.0/docs/playlists.md +87 -0
  19. voce-0.2.0/docs/sync.md +36 -0
  20. voce-0.2.0/docs/voices.md +134 -0
  21. voce-0.2.0/pyproject.toml +61 -0
  22. voce-0.2.0/src/voce/__init__.py +3 -0
  23. voce-0.2.0/src/voce/__main__.py +3 -0
  24. voce-0.2.0/src/voce/cli.py +323 -0
  25. voce-0.2.0/src/voce/commands.py +155 -0
  26. voce-0.2.0/src/voce/document.py +219 -0
  27. voce-0.2.0/src/voce/export.py +115 -0
  28. voce-0.2.0/src/voce/highlight.py +195 -0
  29. voce-0.2.0/src/voce/markdown/__init__.py +6 -0
  30. voce-0.2.0/src/voce/markdown/blocks.py +215 -0
  31. voce-0.2.0/src/voce/markdown/inline.py +154 -0
  32. voce-0.2.0/src/voce/markdown/tables.py +81 -0
  33. voce-0.2.0/src/voce/paths.py +26 -0
  34. voce-0.2.0/src/voce/player.py +234 -0
  35. voce-0.2.0/src/voce/player.swift +90 -0
  36. voce-0.2.0/src/voce/playlist.py +102 -0
  37. voce-0.2.0/src/voce/prepared.py +114 -0
  38. voce-0.2.0/src/voce/sources/__init__.py +35 -0
  39. voce-0.2.0/src/voce/sources/claude.py +78 -0
  40. voce-0.2.0/src/voce/sources/clips.py +75 -0
  41. voce-0.2.0/src/voce/speech/__init__.py +9 -0
  42. voce-0.2.0/src/voce/speech/code.py +341 -0
  43. voce-0.2.0/src/voce/speech/prose.py +43 -0
  44. voce-0.2.0/src/voce/speech/sentences.py +28 -0
  45. voce-0.2.0/src/voce/speech/words.py +217 -0
  46. voce-0.2.0/src/voce/state.py +217 -0
  47. voce-0.2.0/src/voce/sync.py +176 -0
  48. voce-0.2.0/src/voce/ui/__init__.py +1 -0
  49. voce-0.2.0/src/voce/ui/layout.py +111 -0
  50. voce-0.2.0/src/voce/ui/menu.py +74 -0
  51. voce-0.2.0/src/voce/ui/reader.py +507 -0
  52. voce-0.2.0/src/voce/ui/session.py +51 -0
  53. voce-0.2.0/src/voce/ui/terminal.py +153 -0
  54. voce-0.2.0/src/voce/ui/theme.py +141 -0
  55. voce-0.2.0/src/voce/ui/view.py +215 -0
  56. voce-0.2.0/src/voce/voices.py +767 -0
  57. voce-0.2.0/tests/conftest.py +12 -0
  58. voce-0.2.0/tests/test_claude.py +85 -0
  59. voce-0.2.0/tests/test_document.py +100 -0
  60. voce-0.2.0/tests/test_export.py +33 -0
  61. voce-0.2.0/tests/test_highlight.py +45 -0
  62. voce-0.2.0/tests/test_inline.py +69 -0
  63. voce-0.2.0/tests/test_layout.py +37 -0
  64. voce-0.2.0/tests/test_markdown.py +108 -0
  65. voce-0.2.0/tests/test_platform.py +99 -0
  66. voce-0.2.0/tests/test_playlist_state_clips.py +131 -0
  67. voce-0.2.0/tests/test_reader.py +350 -0
  68. voce-0.2.0/tests/test_robustness.py +111 -0
  69. voce-0.2.0/tests/test_speech.py +222 -0
  70. voce-0.2.0/tests/test_sync.py +156 -0
  71. voce-0.2.0/tests/test_theme.py +32 -0
  72. voce-0.2.0/tests/test_voices_cli.py +136 -0
voce-0.2.0/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .DS_Store
10
+ docs/hearing/.corpus/
11
+ .claude/worktrees/
12
+ site/app/py/
13
+ .wrangler/
voce-0.2.0/AGENTS.md ADDED
@@ -0,0 +1,80 @@
1
+ # Working on voce with a coding agent
2
+
3
+ This file is for Claude Code, Codex, Cursor and other agents, and for the people driving them. Read it before
4
+ changing anything. [docs/architecture.md](docs/architecture.md) explains the design in full, and
5
+ [CONTRIBUTING.md](CONTRIBUTING.md) has the human version of the same rules.
6
+
7
+ ## What voce is
8
+
9
+ A terminal app that reads Markdown aloud, highlighting each sentence and word as it is spoken, and reads code the
10
+ way a person says it out loud. Python 3.9+, one runtime dependency (Pygments), plus Kokoro for the local voice.
11
+ It runs on macOS and Linux; the UI is plain `curses`.
12
+
13
+ ## Set up, check, test
14
+
15
+ ```bash
16
+ python3 -m venv .venv
17
+ .venv/bin/pip install -e '.[dev]'
18
+ .venv/bin/ruff check . # must pass
19
+ .venv/bin/pytest # must pass; fast, no network, never touches real state
20
+ .venv/bin/voce --check README.md # how a file will read, with no audio: a quick end-to-end check
21
+ ```
22
+
23
+ CI runs ruff and pytest on macOS and Linux, on Python 3.9 and 3.13. Write code that runs on 3.9
24
+ (`from __future__ import annotations` is used throughout).
25
+
26
+ ## Where things are
27
+
28
+ - `src/voce/speech/words.py`: how code is read, per language, as data. **Most feature requests about how
29
+ something sounds belong here.**
30
+ - `src/voce/speech/code.py`: the tokeniser that turns a line of code into words.
31
+ - `src/voce/speech/prose.py`, `sentences.py`: prose rewrites and sentence splitting.
32
+ - `src/voce/markdown/`: Markdown into blocks of plain text with styled spans.
33
+ - `src/voce/document.py`: blocks into units to say (sentences, code lines, table rows, diff hunks).
34
+ - `src/voce/voices.py`: the voice engines and the audio cache. `player.py`: playback.
35
+ - `src/voce/ui/`: the curses reader. `reader.py` is the state machine, `view.py` the layout and mouse.
36
+ - `src/voce/sources/`: where text comes from (Claude Code transcripts, clipboard, stdin).
37
+ - `site/`: the website and the web app (static, no build step). `relay/`: the web app's sign-in relay.
38
+
39
+ ## Rules that keep voce working
40
+
41
+ - **One plain text per block.** Every block has a single `text` that is shown, spoken and highlighted; styles are
42
+ spans over it. New Markdown features must fit that shape.
43
+ - **Keep the spoken text stable.** Audio is cached by the exact text sent to the voice. Changing what
44
+ `speech.speakable` produces, or how blocks are split into units, makes every user fetch audio again. Do it only
45
+ when it makes the reading better, and say so in the pull request.
46
+ - **Use the manual's own names.** Operators are named as the language's official docs name them (the PHP
47
+ manual's "identical", MDN's "strict equality"), not nicknames.
48
+ - **No network in tests.** Stand in for engines, `gh`, `git` remotes and the clipboard. Tests use `tmp_path`
49
+ and never touch `~/.local/state/voce` or `~/.cache/voce`.
50
+ - **No new runtime dependencies** without a strong reason. Standard library first.
51
+ - **Write for people.** Messages, help and docs use plain words and short sentences. Error messages say what
52
+ to do next.
53
+ - **Add a test** for any change to parsing, splitting, code reading, layout, a source or the reader.
54
+
55
+ ## Adding a language
56
+
57
+ A good first change. In `src/voce/speech/words.py`, add a `Language` with its comment markers, the words it says
58
+ differently and its operator names, then its fence names in `LANGUAGES`:
59
+
60
+ ```python
61
+ RUBY = Language("ruby", comments=("#",), words={"attr_accessor": "attribute accessor"},
62
+ operators={"=>": "hash rocket", "<=>": "spaceship", "&.": "safe navigation"})
63
+ # in LANGUAGES: "ruby": RUBY, "rb": RUBY,
64
+ ```
65
+
66
+ If the language has an operator the tokeniser doesn't know, add it to the `op` alternation in
67
+ `speech/code.py`. Then add cases to `tests/test_speech.py`: a line of code and exactly how it should sound. To
68
+ find what reads badly, run real code through it:
69
+
70
+ ```bash
71
+ .venv/bin/python -c "from voce.speech.code import spoken_line; from voce.speech.words import language; \
72
+ print(spoken_line('puts user&.name', language('ruby')))"
73
+ ```
74
+
75
+ `docs/hearing/corpus.py` does the same over a whole repository of real code.
76
+
77
+ ## Pull requests
78
+
79
+ Keep each one to one change, with a commit message that says what a user notices. Run ruff and pytest
80
+ first. Don't commit generated audio, caches or anything under `.venv`.
@@ -0,0 +1,150 @@
1
+ # Changelog
2
+
3
+ All notable changes to voce are listed here. The format follows [Keep a Changelog](https://keepachangelog.com),
4
+ and versions follow [Semantic Versioning](https://semver.org).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.2.0] - 2026-10-21
9
+
10
+ ### Added
11
+
12
+ - **Linux.** voce runs on Linux as well as macOS. It plays audio with mpv or ffplay, or with `pw-play`,
13
+ `paplay` or `aplay` at one speed, and uses `wl-clipboard`, `xclip` or `xsel` for the clipboard. Kokoro is the
14
+ voice there too. On Windows it runs in WSL. `--plain-player` is the new name for `--afplay`, which still works.
15
+ - **On PyPI.** `pipx install voce` or `uv tool install voce`.
16
+ - **Built to be extended with a coding agent.** `AGENTS.md` (and `CLAUDE.md`, which points to it) tells Claude
17
+ Code, Codex or Cursor how voce is laid out, how to test it and what to keep stable.
18
+ - **Voices that work out of the box.** Kokoro, a small open voice model, runs on the Mac: free, offline, no key.
19
+ The model (350 MB) is downloaded once. It is the voice when no cloud engine is set up; on Python 3.9, where
20
+ Kokoro can't run, the Mac's voices are used as before.
21
+ - **Azure, ElevenLabs and OpenAI** beside Google. Each is used as soon as its key is set (`VOCE_AZURE_KEY` and
22
+ `VOCE_AZURE_REGION`, `VOCE_ELEVENLABS_API_KEY`, `VOCE_OPENAI_API_KEY`), with eight voices to cycle through,
23
+ the month's characters counted against each one's free allowance, and a fallback to Kokoro when one fails.
24
+ `--engine` picks one; `--voice` picks a voice and the engine follows from the name; `voce --voices` shows what
25
+ is set up. <kbd>V</kbd> in the reader moves to the next engine, and <kbd>v</kbd> now cycles the Mac's voices too.
26
+ - **Pick up on another device.** `voce --sync OWNER/REPO` keeps your places, settings and a prepared copy of each
27
+ document in a private git repository, made on GitHub for you when `gh` is signed in; any git URL works too.
28
+ Every run pulls first and pushes when you stop. A prepared copy (`prepared/<hash>.json`) is the document as voce
29
+ shows and says it, so a phone or a browser needs only it and a voice; the reader writes one for whatever it
30
+ opens, and `--prepare` and `--sync` write them for the playlist, with `prepared/index.json` listing them.
31
+ - **A web app**, at [jaystewartuk.github.io/voce/app](https://jaystewartuk.github.io/voce/app/): the same
32
+ library on a phone or in any browser, played by the device's own voice or by Kokoro running in the page, with
33
+ your place written back. Sign in with GitHub and choose the repository; the voce GitHub App reaches only that
34
+ one, and a stateless relay (`relay/`) does the code-for-token swap a page can't. Static files on GitHub Pages
35
+ otherwise. A fine-grained token can be pasted instead.
36
+ - **The web app reads for itself.** voce's reading code runs in the browser (Pyodide), so the app can take a
37
+ playlist README straight from any repository voce can reach, or a Markdown file from the device, and read it
38
+ with nothing prepared on a Mac first. Prepared copies carry a stamp of the reading code and are made again
39
+ when it changes, so a better way of saying something reaches every device.
40
+ - `c` copies the sentence being read and `C` its paragraph; a click on a code block copies the code. Shift-drag
41
+ still selects text the terminal's own way.
42
+ - Inline code is read the same way: `array_values` in a sentence is "array values", and `===` on its own is
43
+ "identical". Before, the voice got the raw text, so study notes had to spell names out in prose.
44
+ - `docs/hearing/check.py` fetches tricky phrases from the real voice, transcribes them with whisper.cpp and
45
+ checks what was heard. It is how rewrites for the voice are verified and new ones found.
46
+ - **Diffs.** A fenced `diff` block in any Markdown is highlighted with added lines in the file's
47
+ syntax, removed lines in red and context dimmed, and read as above. Name the file's language after the word
48
+ diff (```` ```diff python ````) to have the added lines highlighted and read in that language.
49
+ - **Languages.** Code is read in each language's own words. PHP is the full set; Python ("returns", "floor
50
+ divided by", "dunder init"), JavaScript and TypeScript (MDN's "is strictly equal to", "optional chaining",
51
+ "spread"), shell and SQL have starters; Markdown and text are read as prose. Inline code in a sentence is
52
+ read in the language most of the document's code is in. A language is a few lines of data in
53
+ `speech/words.py`.
54
+ - Paths and file names are said with their slashes and the extension by letter: `src/voce/cli.py` is "source
55
+ slash voce slash C L I dot P Y". A line of prose inside code, such as a doc comment, is read as prose.
56
+ - Link reference definitions (`[name]: url`, and the `[//]: # (comment)` idiom) are neither shown nor read.
57
+ - **Doc comments.** The lines of a `/** ... */` block are read as a person reads them: `@param array|string
58
+ $columns The columns` is "parameter array or string, columns, The columns", `@return` is "returns",
59
+ `array<int, string>` is "array of int, string", `\Closure(TValue): bool` is "Closure taking T Value returning
60
+ bool", `{@inheritdoc}` is "inherit doc", and the opening and closing lines are silent.
61
+ - `docs/hearing/corpus.py` reads a whole code base the way voce would and reports the words that came out odd,
62
+ so the rules can be checked against real code. `corpora.txt` names the code bases; laravel/framework is the
63
+ first, and a framework in another language is one more line.
64
+ - `fn` is "arrow function", the manual's name, and the `=>` that follows its parameters is "returns": `fn ($x)
65
+ => $x * 2` is "arrow function x returns x times 2". A `=>` inside the body is still "double arrow".
66
+ - PHP, after reading laravel/framework: `<?php` is "P H P open tag", `#[AsCommand]` is "attribute As Command",
67
+ `$list[] = $item` is "list append item", `$a ? $b : $c` is "a then b else c", `Stringable|string` in a
68
+ signature is "Stringable or string", `instanceof` is "instance of", `endforeach` is "end for each", and
69
+ some two hundred of the standard library's names are said in full, `rtrim` as "R trim" and `realpath` as
70
+ "real path". A string of one symbol is said by name, `'@'` as "an at sign" and `'>='` as "greater than or
71
+ equal to", and `"{$this->name}"` inside a string is read as code, and a path in a string,
72
+ `'routes/web.php'`, as a path. An abbreviation inside a camel-case name
73
+ is said as it would be on its own: `expectsJson` is "expects JSON", `getId` is "get I D".
74
+
75
+ ### Changed
76
+
77
+ - **Pausing in the web app holds your place in the sentence.** With the Kokoro voice, playing again within half a
78
+ minute picks up about a second before where you stopped, at the gap before a word, so the cut-off word is heard
79
+ whole. After a longer pause, or with the device's own voice, the sentence starts again as before.
80
+ - **The web app's play button is the whole player bar** between the previous and next arrows, so you can pause
81
+ without looking. Tapping the sentence being read plays or pauses too; tapping any other sentence still goes there.
82
+ - **The web app marks the word being read** inside the highlighted sentence, from how long Kokoro makes each
83
+ sound (or from the device's own voice, where it reports words). A line of code said differently from how it's
84
+ shown keeps only its line marked. It can be turned off in settings. Kokoro now downloads the timestamped copy of
85
+ the same model, once.
86
+ - **State is laid out for syncing.** Your place in each document is its own small file under
87
+ `~/.local/state/voce/places/`, named by a hash of the document's text rather than its path, so the same file
88
+ resumes on any machine however it got there. Settings and the month's character counts have their own files.
89
+ Point `VOCE_STATE` at a synced folder to pick up on another Mac where you left off. An existing `state.json` is
90
+ migrated once and kept as `state-v1.json`.
91
+ - The usage file counts characters per engine. Existing counts are kept as Google's.
92
+ - The code is arranged for people who want to extend it: `markdown/` (inline, blocks, tables), `speech/`
93
+ (sentences, prose, code, the word tables), `sources/` (Claude's reply, the clipboard), `commands.py`
94
+ for the printing commands, `ui/session.py` for the curses session and `ui/view.py` for where things sit on
95
+ the screen, leaving `ui/reader.py` to what happens in time. Every source raises one `SourceError`.
96
+ - The text sent to the voice changes for inline code with a slash, a file name or an operator in it, and for
97
+ text with a middle dot or a diff count such as "(+12 −3)", so those sentences are fetched again once.
98
+ - Drag across any text to copy it, without moving the voice. The text holds still while the button is down,
99
+ the selection shows as you drag, and it is on the clipboard when you let go. Whole blocks come out as they
100
+ are in the document, so a drag down a code block copies real lines.
101
+ - The title bar fills from the left as the file is read, and the status bar says how far through you are as a
102
+ percentage, in place of the sentence count.
103
+ - The sentence being read stays on one row, two fifths of the way down, and the text glides up to meet it a
104
+ line or so a frame instead of jumping when it drifted too far. Clicking a sentence or carrying on after a
105
+ pause brings it to the same row, and the end of a file rises to it too, so the eye never has to drop.
106
+ - Code blocks are read, line by line, the way a pairing partner says code aloud: names as words (`array_values`
107
+ is "array values", `usort` is "U sort"), operators by the PHP manual's names (`===` is "is identical to", `==` is
108
+ "is equal to", `=>` is "double arrow", `??` is "null coalescing", `<=>` is "spaceship", `++` is "increment"), brackets silent,
109
+ strings by their contents, and a trailing comment as prose after a short pause. The line being read sits on the band and the word being
110
+ spoken is marked, as in prose, and a click on a line of code starts reading there. `--skip-code` keeps the
111
+ old behaviour, showing code without reading it, where a click still copies the block.
112
+
113
+ ### Fixed
114
+
115
+ - PHP code blocks are highlighted even when they do not start with an opening `<?php` tag, as snippets in notes
116
+ rarely do.
117
+ - "PHP 8" is read as the language version, not as eight Philippine pesos; likewise BSD, CRC and CVE before a
118
+ version-like number. Sums of money such as "PHP 500" are still money.
119
+ - The mouse wheel scrolls down as well as up on macOS, whose own ncurses has no fifth button and reports
120
+ wheel-down differently.
121
+ - Pausing just as a sentence ends, in the short silence after it, no longer plays that sentence again when you
122
+ carry on: the reader moves to the next one.
123
+
124
+ ## [0.1.0] - 2026-10-17
125
+
126
+ The first public release.
127
+
128
+ ### Added
129
+
130
+ - Reads Markdown aloud in the terminal, highlighting the sentence being read and the word being spoken.
131
+ - Shows inline styles (bold, italic, code, links, strikethrough), aligned tables, task lists and syntax-highlighted
132
+ code. Code is shown but not read. Tables are read row by row.
133
+ - `voce -r` reads Claude Code's latest reply in the current folder, straight from the session transcript.
134
+ - `voce -c` reads the clipboard, and `voce -` reads standard input.
135
+ - Several files in one go: `voce a.md b.md`.
136
+ - Mac voices out of the box. Google Chirp 3 HD voices with an API key or gcloud, eight to cycle through, any
137
+ language.
138
+ - A per-sentence audio cache, so listening again is free and works offline.
139
+ - A native Swift player for gapless playback, a true pause and a word highlight that keeps time, with `afplay` as
140
+ a fallback.
141
+ - Your place is kept per file and found again by its text after edits. Files reload when they change on disk.
142
+ - Playlists from a README's "Listen in this order" section, with a menu, progress, parts and quizzes.
143
+ - `--export` writes files, a reply, the clipboard or a playlist as one MP3.
144
+ - `--prepare` fetches audio ahead of time, `--check` reports how files will read, and `--minutes` keeps playlist
145
+ durations current.
146
+ - A click on a sentence jumps to it. The mouse wheel scrolls.
147
+
148
+ [Unreleased]: https://github.com/jaystewartuk/voce/compare/v0.2.0...HEAD
149
+ [0.2.0]: https://github.com/jaystewartuk/voce/releases/tag/v0.2.0
150
+ [0.1.0]: https://github.com/jaystewartuk/voce/releases/tag/v0.1.0
voce-0.2.0/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,93 @@
1
+ # Contributing to voce
2
+
3
+ Thank you for helping. Bug reports, ideas and pull requests are all welcome, including ones written with a coding
4
+ agent: point it at [AGENTS.md](AGENTS.md) first.
5
+
6
+ ## Set up
7
+
8
+ ```bash
9
+ git clone https://github.com/jaystewartuk/voce
10
+ cd voce
11
+ python3 -m venv .venv
12
+ .venv/bin/pip install -e '.[dev]'
13
+ .venv/bin/voce README.md
14
+ ```
15
+
16
+ ## Before you open a pull request
17
+
18
+ ```bash
19
+ .venv/bin/ruff check .
20
+ .venv/bin/pytest
21
+ ```
22
+
23
+ Both run in CI on macOS and Linux. The tests never touch your real state or cache, and never call Google.
24
+
25
+ Some guidelines:
26
+
27
+ - **Keep the plain-text model.** Every block has one plain `text` that is shown, spoken and highlighted; styles
28
+ are spans over it. New Markdown features should fit that shape. See [docs/architecture.md](docs/architecture.md).
29
+ - **Keep the voice's text stable.** Audio is cached by the exact text sent to the voice. A change to
30
+ `speech.speakable` or to how blocks are split makes everyone re-fetch audio, so make such changes on purpose.
31
+ - **Add a test** for any change to parsing, sentence splitting, code reading, layout, a source or the reader.
32
+ - **Write for people.** Messages, help text and docs use plain words and short sentences.
33
+
34
+ ## Adding a language
35
+
36
+ How code is read is data in `src/voce/speech/words.py`. A language is a `Language` with the words it says
37
+ differently from the common ones, its operator names, and how its comments start:
38
+
39
+ ```python
40
+ RUBY = Language("ruby", comments=("#",), words={"attr_accessor": "attribute accessor", "puts": "puts"},
41
+ operators={"=>": "hash rocket", "<=>": "spaceship", "&.": "safe navigation"})
42
+ ```
43
+
44
+ Then add its fence labels and Pygments aliases to `LANGUAGES` (`"ruby": RUBY, "rb": RUBY`). Code fences, diffs
45
+ of `.rb` files and inline code in documents about Ruby will use it. If the language has an operator the
46
+ tokeniser in `speech/code.py` doesn't know, add it to the `op` alternation there. Use the names from the
47
+ language's own manual, and add a line to `tests/test_speech.py` with a line of code and how it should sound.
48
+
49
+ ## Adding a source
50
+
51
+ A source is a module in `src/voce/sources/` that turns something into a Markdown file and raises a `SourceError`
52
+ when it can't. `sources.resolve` is the one place the command line is turned into a file; add a flag to
53
+ `cli.parser` and a branch there. Keep the code that talks to the outside world apart from the code that writes
54
+ the Markdown, and stand the outside world in for in tests so none of them needs the network.
55
+
56
+ ## Re-recording the demo
57
+
58
+ The GIF in the README and the screenshot are made with [vhs](https://github.com/charmbracelet/vhs):
59
+
60
+ ```bash
61
+ brew install vhs
62
+ docs/demo/record.sh
63
+ ```
64
+
65
+ The script reads `docs/demo/reply.md` through a throwaway Claude Code transcript, with empty state and cache
66
+ folders, so nothing from your machine appears in the recording.
67
+
68
+ ## Checking what the voice says
69
+
70
+ Voices misread some text: Google turns "PHP 8" into "eight Philippine pesos". `speech.speakable` rewrites such
71
+ text before it is spoken, and `docs/hearing/check.py` checks those rewrites by ear: it fetches each phrase in
72
+ `docs/hearing/phrases.txt` from the real voice, transcribes it with whisper.cpp and compares.
73
+
74
+ ```bash
75
+ brew install whisper-cpp ffmpeg
76
+ curl -LO https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin
77
+ .venv/bin/python docs/hearing/check.py --model ggml-base.en.bin
78
+ ```
79
+
80
+ Run it with `--raw` to hear the text as written, which is how to confirm a misreading before adding a rewrite.
81
+ It uses your Google voice and counts towards your monthly characters, so it is not part of the test suite.
82
+
83
+ ## The website
84
+
85
+ The site in `site/` is a single static page with no build step. Open `site/index.html` in a browser to work on it.
86
+ It is published to GitHub Pages by `.github/workflows/pages.yml` on every push to `main` that touches `site/`.
87
+
88
+ ## Releasing
89
+
90
+ 1. Update `__version__` in `src/voce/__init__.py` and move `CHANGELOG.md`'s Unreleased section under the version.
91
+ 2. Commit, then tag and push: `git tag v0.2.0 && git push origin main v0.2.0`.
92
+ 3. The `release` workflow builds the package, checks the tag matches the version, publishes to PyPI with
93
+ trusted publishing (no token is stored anywhere) and makes the GitHub release from the changelog section.
voce-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jay Stewart
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.