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.
- voce-0.2.0/.gitignore +13 -0
- voce-0.2.0/AGENTS.md +80 -0
- voce-0.2.0/CHANGELOG.md +150 -0
- voce-0.2.0/CLAUDE.md +1 -0
- voce-0.2.0/CONTRIBUTING.md +93 -0
- voce-0.2.0/LICENSE +21 -0
- voce-0.2.0/PKG-INFO +332 -0
- voce-0.2.0/README.md +303 -0
- voce-0.2.0/SECURITY.md +11 -0
- voce-0.2.0/docs/architecture.md +195 -0
- voce-0.2.0/docs/brand.md +44 -0
- voce-0.2.0/docs/examples/laravel.md +229 -0
- voce-0.2.0/docs/google-voices.md +3 -0
- voce-0.2.0/docs/hearing/check.py +117 -0
- voce-0.2.0/docs/hearing/corpora.txt +2 -0
- voce-0.2.0/docs/hearing/corpus.py +211 -0
- voce-0.2.0/docs/hearing/phrases.txt +51 -0
- voce-0.2.0/docs/playlists.md +87 -0
- voce-0.2.0/docs/sync.md +36 -0
- voce-0.2.0/docs/voices.md +134 -0
- voce-0.2.0/pyproject.toml +61 -0
- voce-0.2.0/src/voce/__init__.py +3 -0
- voce-0.2.0/src/voce/__main__.py +3 -0
- voce-0.2.0/src/voce/cli.py +323 -0
- voce-0.2.0/src/voce/commands.py +155 -0
- voce-0.2.0/src/voce/document.py +219 -0
- voce-0.2.0/src/voce/export.py +115 -0
- voce-0.2.0/src/voce/highlight.py +195 -0
- voce-0.2.0/src/voce/markdown/__init__.py +6 -0
- voce-0.2.0/src/voce/markdown/blocks.py +215 -0
- voce-0.2.0/src/voce/markdown/inline.py +154 -0
- voce-0.2.0/src/voce/markdown/tables.py +81 -0
- voce-0.2.0/src/voce/paths.py +26 -0
- voce-0.2.0/src/voce/player.py +234 -0
- voce-0.2.0/src/voce/player.swift +90 -0
- voce-0.2.0/src/voce/playlist.py +102 -0
- voce-0.2.0/src/voce/prepared.py +114 -0
- voce-0.2.0/src/voce/sources/__init__.py +35 -0
- voce-0.2.0/src/voce/sources/claude.py +78 -0
- voce-0.2.0/src/voce/sources/clips.py +75 -0
- voce-0.2.0/src/voce/speech/__init__.py +9 -0
- voce-0.2.0/src/voce/speech/code.py +341 -0
- voce-0.2.0/src/voce/speech/prose.py +43 -0
- voce-0.2.0/src/voce/speech/sentences.py +28 -0
- voce-0.2.0/src/voce/speech/words.py +217 -0
- voce-0.2.0/src/voce/state.py +217 -0
- voce-0.2.0/src/voce/sync.py +176 -0
- voce-0.2.0/src/voce/ui/__init__.py +1 -0
- voce-0.2.0/src/voce/ui/layout.py +111 -0
- voce-0.2.0/src/voce/ui/menu.py +74 -0
- voce-0.2.0/src/voce/ui/reader.py +507 -0
- voce-0.2.0/src/voce/ui/session.py +51 -0
- voce-0.2.0/src/voce/ui/terminal.py +153 -0
- voce-0.2.0/src/voce/ui/theme.py +141 -0
- voce-0.2.0/src/voce/ui/view.py +215 -0
- voce-0.2.0/src/voce/voices.py +767 -0
- voce-0.2.0/tests/conftest.py +12 -0
- voce-0.2.0/tests/test_claude.py +85 -0
- voce-0.2.0/tests/test_document.py +100 -0
- voce-0.2.0/tests/test_export.py +33 -0
- voce-0.2.0/tests/test_highlight.py +45 -0
- voce-0.2.0/tests/test_inline.py +69 -0
- voce-0.2.0/tests/test_layout.py +37 -0
- voce-0.2.0/tests/test_markdown.py +108 -0
- voce-0.2.0/tests/test_platform.py +99 -0
- voce-0.2.0/tests/test_playlist_state_clips.py +131 -0
- voce-0.2.0/tests/test_reader.py +350 -0
- voce-0.2.0/tests/test_robustness.py +111 -0
- voce-0.2.0/tests/test_speech.py +222 -0
- voce-0.2.0/tests/test_sync.py +156 -0
- voce-0.2.0/tests/test_theme.py +32 -0
- voce-0.2.0/tests/test_voices_cli.py +136 -0
voce-0.2.0/.gitignore
ADDED
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`.
|
voce-0.2.0/CHANGELOG.md
ADDED
|
@@ -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.
|