anki-voice 1.0.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.
- anki_voice-1.0.0/.github/workflows/publish.yml +34 -0
- anki_voice-1.0.0/.github/workflows/test.yml +20 -0
- anki_voice-1.0.0/.gitignore +5 -0
- anki_voice-1.0.0/LICENSE +21 -0
- anki_voice-1.0.0/PKG-INFO +288 -0
- anki_voice-1.0.0/README.md +268 -0
- anki_voice-1.0.0/pyproject.toml +41 -0
- anki_voice-1.0.0/src/anki_voice/__init__.py +0 -0
- anki_voice-1.0.0/src/anki_voice/anki.py +141 -0
- anki_voice-1.0.0/src/anki_voice/cards.py +75 -0
- anki_voice-1.0.0/src/anki_voice/guard.py +38 -0
- anki_voice-1.0.0/src/anki_voice/oauth.py +273 -0
- anki_voice-1.0.0/src/anki_voice/review.py +235 -0
- anki_voice-1.0.0/src/anki_voice/server.py +248 -0
- anki_voice-1.0.0/src/anki_voice/session.py +288 -0
- anki_voice-1.0.0/tests/test_cli.py +12 -0
- anki_voice-1.0.0/tests/test_guard.py +56 -0
- anki_voice-1.0.0/tests/test_oauth.py +182 -0
- anki_voice-1.0.0/tests/test_review.py +424 -0
- anki_voice-1.0.0/tests/test_session.py +286 -0
- anki_voice-1.0.0/uv.lock +821 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Push a tag v<version> (matching pyproject.toml) to publish that version to PyPI.
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
uses: ./.github/workflows/test.yml
|
|
14
|
+
|
|
15
|
+
publish:
|
|
16
|
+
needs: test
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
# PyPI trusted publishing: no API token, PyPI trusts this workflow in this environment.
|
|
19
|
+
environment: pypi
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
id-token: write
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: astral-sh/setup-uv@v6
|
|
26
|
+
- name: Check that the tag matches the version
|
|
27
|
+
run: |
|
|
28
|
+
version=$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
|
|
29
|
+
if [ "v$version" != "$GITHUB_REF_NAME" ]; then
|
|
30
|
+
echo "Tag $GITHUB_REF_NAME doesn't match version $version in pyproject.toml" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
- run: uv build
|
|
34
|
+
- run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_call:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
# anki-voice runs on macOS only.
|
|
15
|
+
runs-on: macos-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v6
|
|
19
|
+
- run: uv sync --locked
|
|
20
|
+
- run: uv run pytest
|
anki_voice-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christian Fratta
|
|
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.
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: anki-voice
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP server for reviewing Anki cards by voice from the Claude app
|
|
5
|
+
Project-URL: Repository, https://github.com/EquineOntology/anki-voice
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: anki,claude,flashcards,mcp,voice
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Environment :: MacOS X
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Education
|
|
14
|
+
Requires-Python: >=3.12
|
|
15
|
+
Requires-Dist: httpx>=0.27
|
|
16
|
+
Requires-Dist: mcp<3,>=2.3
|
|
17
|
+
Requires-Dist: starlette>=0.40
|
|
18
|
+
Requires-Dist: uvicorn>=0.30
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# anki-voice
|
|
22
|
+
|
|
23
|
+
Hands-free Anki review via Claude voice mode.
|
|
24
|
+
|
|
25
|
+
Claude reads each card's front, listens to your answer, tells you whether it
|
|
26
|
+
was right, and records the grade in Anki. It works by running a small server
|
|
27
|
+
on your Mac next to Anki and Claude connects to it as a custom connector.
|
|
28
|
+
|
|
29
|
+
## Setup
|
|
30
|
+
|
|
31
|
+
Your agent can take care of the setup, except for a few like generating a
|
|
32
|
+
password, signing up for Tailscale, etc.
|
|
33
|
+
|
|
34
|
+
### Prereqs
|
|
35
|
+
|
|
36
|
+
- A Mac: anki-voice runs on macOS only, `serve` included
|
|
37
|
+
- [Anki](https://apps.ankiweb.net) with a recent
|
|
38
|
+
[AnkiConnect](https://foosoft.net/projects/anki-connect/) add-on, one that
|
|
39
|
+
has its JSON-RPC API v1 (see [Checking AnkiConnect](#checking-ankiconnect))
|
|
40
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
41
|
+
- [Tailscale](https://tailscale.com), with
|
|
42
|
+
MagicDNS and [Funnel](https://tailscale.com/kb/1223/funnel) enabled for your tailnet. It
|
|
43
|
+
puts the server on the internet for the length of a review, so Claude can
|
|
44
|
+
reach it. You can do this on a free account.
|
|
45
|
+
- A place to keep the sign-in password, ideally a password manager with a
|
|
46
|
+
command-line tool
|
|
47
|
+
- A Claude account that can add custom connectors
|
|
48
|
+
|
|
49
|
+
### First-time setup
|
|
50
|
+
|
|
51
|
+
1. **Install it**:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv tool install anki-voice
|
|
55
|
+
```
|
|
56
|
+
2. **Choose a password** of at least 16 characters. Claude will ask for it
|
|
57
|
+
the first time it connects. Tell the server how to get it by setting, in
|
|
58
|
+
your shell profile, a command that prints it:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export ANKI_VOICE_PASSWORD_COMMAND='<your password manager> get anki-voice'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
or the password itself, in `ANKI_VOICE_PASSWORD`. Prefer the command: the
|
|
65
|
+
password then exists only in the server's memory, while
|
|
66
|
+
`ANKI_VOICE_PASSWORD` keeps it in plain text in your profile and in the
|
|
67
|
+
environment of everything started from your shell.
|
|
68
|
+
3. **Add the instructions field** to your note types:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
anki-voice add-field
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This adds an `AnkiVoice instructions` field to every note type that lacks
|
|
75
|
+
it (see [Instructions for single cards](#instructions-for-single-cards)).
|
|
76
|
+
Changing a note type needs a full sync, so sync your other devices first,
|
|
77
|
+
then choose "upload" at the next sync. Run it again after you create a
|
|
78
|
+
new note type.
|
|
79
|
+
4. **Name the languages you study**, if any, so their cards are read in that
|
|
80
|
+
language (see [Cards in other languages](#cards-in-other-languages)):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
export ANKI_VOICE_LANGUAGES='Spanish 🇪🇸 🇲🇽'
|
|
84
|
+
```
|
|
85
|
+
5. **Leave out decks** that don't work by voice, if you like:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
export ANKI_VOICE_EXCLUDE_DECKS='Geography::Flags,Music'
|
|
89
|
+
```
|
|
90
|
+
6. **Add the connector in Claude** once a session is online (see below): add a
|
|
91
|
+
custom connector with the address the session prints, and choose **Sign in
|
|
92
|
+
now** and **Register automatically**. A sign-in page opens; enter the
|
|
93
|
+
password.
|
|
94
|
+
|
|
95
|
+
## Run a review
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
anki-voice session
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then **wait until it prints `online at https://…/mcp`**. This often takes 10
|
|
102
|
+
minutes or more the first time in a while (see
|
|
103
|
+
[Why the wait](#why-the-wait)). Connecting earlier fails.
|
|
104
|
+
|
|
105
|
+
Once it's online, start voice mode in Claude and say something like "let's do
|
|
106
|
+
my Anki review" or "just my Spanish cards".
|
|
107
|
+
|
|
108
|
+
The session stops on its own two minutes after the review is done, or after 30
|
|
109
|
+
minutes without activity. Ctrl-C or closing the terminal stops it too. Either
|
|
110
|
+
way, the server goes offline and nothing is left on the internet.
|
|
111
|
+
|
|
112
|
+
### What you can say
|
|
113
|
+
|
|
114
|
+
| Say | What happens |
|
|
115
|
+
|---|---|
|
|
116
|
+
| your answer | Claude says whether it was right, gives the correct answer if not, and moves on |
|
|
117
|
+
| "I don't know" | Counts as wrong ("again") |
|
|
118
|
+
| your answer and "easy", or "that one was hard" | A correct answer is graded "easy" or "hard" instead of "good". Said after the grade, it changes that grade |
|
|
119
|
+
| "skip" | The card stays due in Anki; you get the next one |
|
|
120
|
+
| "don't ask me this one again" | The card's note gets the tag `anki-voice::skip` and voice reviews leave it out from now on. Remove the tag in Anki to bring it back |
|
|
121
|
+
| "undo", or "that was right!" | Takes back the last grade or skip, once |
|
|
122
|
+
| "flag this red" | Flags the card in Anki for you to look at later. Claude asks for a colour if you don't name one |
|
|
123
|
+
| "on this card, accept either order" | Claude rewrites the card's instructions (see below) |
|
|
124
|
+
| "just maths" / "everything again" | Limits the review to one topic, or lifts the limit |
|
|
125
|
+
|
|
126
|
+
Grading is strict: only a correct and complete answer is "good" (or "hard" or
|
|
127
|
+
"easy", when you ask). For nouns in languages whose articles show gender, the
|
|
128
|
+
article is part of the answer.
|
|
129
|
+
|
|
130
|
+
### Cards in other languages
|
|
131
|
+
|
|
132
|
+
Each language in `ANKI_VOICE_LANGUAGES` is a name followed by any flag emoji
|
|
133
|
+
that mark its cards. A card is in that language when its deck or note type name
|
|
134
|
+
contains the name (`Spanish`, `Study::Spanish`, `Spanish vocab`), or its front
|
|
135
|
+
or back shows one of the flags. Separate languages with commas:
|
|
136
|
+
`'Spanish 🇪🇸 🇲🇽, French 🇫🇷'`.
|
|
137
|
+
|
|
138
|
+
Claude reads these cards with its voice for that language, and speaks only
|
|
139
|
+
that language on them. Or that's how it works most of the time, it's a bit
|
|
140
|
+
finicky. If it doesn't work well, try doing a session only on that deck.
|
|
141
|
+
|
|
142
|
+
### Instructions for single cards
|
|
143
|
+
|
|
144
|
+
The `AnkiVoice instructions` field on a note tells Claude how to treat its
|
|
145
|
+
cards: what counts as an answer, how to read the prompt. Claude follows them
|
|
146
|
+
over its general rules and never reads them aloud. No card template shows the
|
|
147
|
+
field, so the cards look the same in Anki.
|
|
148
|
+
|
|
149
|
+
You can write them in Anki, or ask Claude during a review ("on this card,
|
|
150
|
+
accept either order").
|
|
151
|
+
|
|
152
|
+
### While a session runs
|
|
153
|
+
|
|
154
|
+
**Don't use Anki's window.** Two reasons:
|
|
155
|
+
|
|
156
|
+
- "undo" uses Anki's own undo, which takes back whatever changed last, even if
|
|
157
|
+
that was you in the window.
|
|
158
|
+
- Once Anki's window shows a deck or its reviewer, Anki only accepts answers
|
|
159
|
+
for the card at the top of its own queue. The session puts the window back on
|
|
160
|
+
the Decks screen when it starts, and again if Anki refuses an answer.
|
|
161
|
+
|
|
162
|
+
### Which cards come up
|
|
163
|
+
|
|
164
|
+
1. Cards in learning that are due now
|
|
165
|
+
2. Reviews due, shuffled within each due day
|
|
166
|
+
3. Cards in learning due within the next 20 minutes
|
|
167
|
+
|
|
168
|
+
New cards **do not** come up. Neither do suspended or buried cards, cards from
|
|
169
|
+
excluded decks, or cards whose front needs a picture or a sound. Claude tells
|
|
170
|
+
you at the end how many of those last were left out.
|
|
171
|
+
|
|
172
|
+
Each grade is saved in Anki the moment it's given, so a card you got wrong
|
|
173
|
+
comes back when its learning step makes it due. Anki logs each answer with the
|
|
174
|
+
time from when Claude got the card to when it was graded, less 10 seconds for
|
|
175
|
+
the speech you can't skip.
|
|
176
|
+
|
|
177
|
+
## Technical details
|
|
178
|
+
|
|
179
|
+
### Security
|
|
180
|
+
|
|
181
|
+
- **Online only during a review.** The Funnel and its public address exist only
|
|
182
|
+
while a session runs. A small guard process takes the Funnel down as soon as
|
|
183
|
+
the session is gone, even after `kill -9`. To check that nothing is
|
|
184
|
+
published:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
tailscale funnel status
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
It should say "No serve config". If only the Mac app is installed, the
|
|
191
|
+
command is `/Applications/Tailscale.app/Contents/MacOS/Tailscale`.
|
|
192
|
+
- **Password sign-in.** The server signs Claude in itself, with one password.
|
|
193
|
+
Every request without a valid token is refused.
|
|
194
|
+
- **Only Claude can sign in.** The sign-in only ever sends its result to
|
|
195
|
+
Claude's own address, so a link to the sign-in page can't be used to phish
|
|
196
|
+
the password into someone else's hands.
|
|
197
|
+
- **Tokens expire.** Claude's access lasts an hour and is renewed for up to 48
|
|
198
|
+
hours of disuse; after that, it asks for the password again.
|
|
199
|
+
- **Tokens are stored as hashes**, in `~/.local/share/anki-voice/oauth.json`
|
|
200
|
+
(readable only by you), so restarting doesn't sign Claude out. Delete that
|
|
201
|
+
file to sign everything out.
|
|
202
|
+
- **Guessing is limited.** After 10 wrong passwords in 15 minutes, sign-in is
|
|
203
|
+
refused for the rest of that window. The limit is shared by everyone, so
|
|
204
|
+
someone who finds the address can keep you from signing in for as long as
|
|
205
|
+
they keep guessing. Claude stays connected if it already is.
|
|
206
|
+
- **Card text reaches Claude as-is.** Each card's front, back and
|
|
207
|
+
`AnkiVoice instructions` field go to Claude, including text a template
|
|
208
|
+
hides, and Claude follows the instructions field over its general rules. A
|
|
209
|
+
shared deck written by someone else could use that to have Claude misgrade
|
|
210
|
+
cards or rewrite other notes' instructions. Check the instructions field of
|
|
211
|
+
shared decks before you review them by voice, or leave them out with
|
|
212
|
+
`ANKI_VOICE_EXCLUDE_DECKS`.
|
|
213
|
+
|
|
214
|
+
### Why the wait when starting a session
|
|
215
|
+
|
|
216
|
+
Tailscale publishes the server's public address in DNS only when the Funnel
|
|
217
|
+
starts, and the record often takes 10 minutes or more to appear. If anything
|
|
218
|
+
looks the address up before then, such as Claude when you try to connect, the
|
|
219
|
+
answer "no such name" is cached for up to 5 minutes. So the session waits until
|
|
220
|
+
the record exists, then until any such cached answer has expired, then until a
|
|
221
|
+
request through the public relay actually reaches the server. Only then does it
|
|
222
|
+
say it's online. After that it checks every minute and tells you if the server
|
|
223
|
+
stops answering.
|
|
224
|
+
|
|
225
|
+
### Other ways to run it
|
|
226
|
+
|
|
227
|
+
**Behind a tunnel of your own**, instead of Tailscale:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
export ANKI_VOICE_PUBLIC_URL=https://<your tunnel's address>
|
|
231
|
+
anki-voice serve
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
It gets the password the same way a session does. The server listens on
|
|
235
|
+
`127.0.0.1:8787`; point the tunnel there. The connector
|
|
236
|
+
address is `$ANKI_VOICE_PUBLIC_URL/mcp`. Unlike a session, this runs until you
|
|
237
|
+
stop it.
|
|
238
|
+
|
|
239
|
+
**For a local client** such as Claude Code, without sign-in:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
anki-voice serve --stdio
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Settings
|
|
246
|
+
|
|
247
|
+
All settings are environment variables.
|
|
248
|
+
|
|
249
|
+
| Variable | Default | What it sets |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| `ANKI_VOICE_PASSWORD_COMMAND` | | A command that prints the sign-in password |
|
|
252
|
+
| `ANKI_VOICE_PASSWORD` | | The password itself, instead of a command |
|
|
253
|
+
| `ANKI_VOICE_LANGUAGES` | none | Languages to read in their own voice, each with its flags, comma-separated |
|
|
254
|
+
| `ANKI_VOICE_EXCLUDE_DECKS` | none | Decks to leave out, with their subdecks, comma-separated |
|
|
255
|
+
| `ANKI_VOICE_IDLE_MINUTES` | `30` | Minutes without activity before a session stops |
|
|
256
|
+
| `ANKI_VOICE_LEARN_AHEAD_MINUTES` | `20` | How far ahead cards in learning come up |
|
|
257
|
+
| `ANKI_VOICE_OVERHEAD_SECONDS` | `10` | Taken off each answer time logged in Anki |
|
|
258
|
+
| `ANKI_VOICE_DRY_RUN` | | `1` to try things out: grades, instructions and flags aren't saved to Anki |
|
|
259
|
+
| `ANKI_VOICE_PORT` | `8787` | Local port of the server |
|
|
260
|
+
| `ANKI_VOICE_STATE_FILE` | `~/.local/share/anki-voice/oauth.json` | Where tokens are stored |
|
|
261
|
+
| `ANKICONNECT_URL` | `http://127.0.0.1:8765` | Where AnkiConnect listens |
|
|
262
|
+
|
|
263
|
+
### Checking AnkiConnect
|
|
264
|
+
|
|
265
|
+
anki-voice uses AnkiConnect's long-standing API (version 6) to read cards and
|
|
266
|
+
change notes, and its newer JSON-RPC API v1 (at `/api/v1`) to grade, flag and
|
|
267
|
+
undo. Older AnkiConnect releases have only the first. To check yours, with Anki
|
|
268
|
+
open:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
curl -s localhost:8765/api/v1 -d '{"jsonrpc":"2.0","id":1,"method":"operations.get","params":{"operationId":"check"}}'
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
A reply that starts with `{"jsonrpc"` (here, "Operation not found") means API v1
|
|
275
|
+
is there. anki-voice is tested with the AnkiConnect release of October 2026,
|
|
276
|
+
which needs Anki 26.8 or later.
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
## For developers
|
|
280
|
+
|
|
281
|
+
- How Claude runs a review is set by the instructions at the top of
|
|
282
|
+
`src/anki_voice/server.py`.
|
|
283
|
+
|
|
284
|
+
- You can run tests via uv: `uv run pytest`, and anki-voice from the
|
|
285
|
+
checkout with `uv run anki-voice …`.
|
|
286
|
+
- To release: set the new version in `pyproject.toml` (and `uv lock`), commit,
|
|
287
|
+
then push a tag `v<version>`. GitHub Actions runs the tests, and publishes to
|
|
288
|
+
PyPI only if the tag matches the version.
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# anki-voice
|
|
2
|
+
|
|
3
|
+
Hands-free Anki review via Claude voice mode.
|
|
4
|
+
|
|
5
|
+
Claude reads each card's front, listens to your answer, tells you whether it
|
|
6
|
+
was right, and records the grade in Anki. It works by running a small server
|
|
7
|
+
on your Mac next to Anki and Claude connects to it as a custom connector.
|
|
8
|
+
|
|
9
|
+
## Setup
|
|
10
|
+
|
|
11
|
+
Your agent can take care of the setup, except for a few like generating a
|
|
12
|
+
password, signing up for Tailscale, etc.
|
|
13
|
+
|
|
14
|
+
### Prereqs
|
|
15
|
+
|
|
16
|
+
- A Mac: anki-voice runs on macOS only, `serve` included
|
|
17
|
+
- [Anki](https://apps.ankiweb.net) with a recent
|
|
18
|
+
[AnkiConnect](https://foosoft.net/projects/anki-connect/) add-on, one that
|
|
19
|
+
has its JSON-RPC API v1 (see [Checking AnkiConnect](#checking-ankiconnect))
|
|
20
|
+
- [uv](https://docs.astral.sh/uv/)
|
|
21
|
+
- [Tailscale](https://tailscale.com), with
|
|
22
|
+
MagicDNS and [Funnel](https://tailscale.com/kb/1223/funnel) enabled for your tailnet. It
|
|
23
|
+
puts the server on the internet for the length of a review, so Claude can
|
|
24
|
+
reach it. You can do this on a free account.
|
|
25
|
+
- A place to keep the sign-in password, ideally a password manager with a
|
|
26
|
+
command-line tool
|
|
27
|
+
- A Claude account that can add custom connectors
|
|
28
|
+
|
|
29
|
+
### First-time setup
|
|
30
|
+
|
|
31
|
+
1. **Install it**:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install anki-voice
|
|
35
|
+
```
|
|
36
|
+
2. **Choose a password** of at least 16 characters. Claude will ask for it
|
|
37
|
+
the first time it connects. Tell the server how to get it by setting, in
|
|
38
|
+
your shell profile, a command that prints it:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export ANKI_VOICE_PASSWORD_COMMAND='<your password manager> get anki-voice'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
or the password itself, in `ANKI_VOICE_PASSWORD`. Prefer the command: the
|
|
45
|
+
password then exists only in the server's memory, while
|
|
46
|
+
`ANKI_VOICE_PASSWORD` keeps it in plain text in your profile and in the
|
|
47
|
+
environment of everything started from your shell.
|
|
48
|
+
3. **Add the instructions field** to your note types:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
anki-voice add-field
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This adds an `AnkiVoice instructions` field to every note type that lacks
|
|
55
|
+
it (see [Instructions for single cards](#instructions-for-single-cards)).
|
|
56
|
+
Changing a note type needs a full sync, so sync your other devices first,
|
|
57
|
+
then choose "upload" at the next sync. Run it again after you create a
|
|
58
|
+
new note type.
|
|
59
|
+
4. **Name the languages you study**, if any, so their cards are read in that
|
|
60
|
+
language (see [Cards in other languages](#cards-in-other-languages)):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export ANKI_VOICE_LANGUAGES='Spanish 🇪🇸 🇲🇽'
|
|
64
|
+
```
|
|
65
|
+
5. **Leave out decks** that don't work by voice, if you like:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export ANKI_VOICE_EXCLUDE_DECKS='Geography::Flags,Music'
|
|
69
|
+
```
|
|
70
|
+
6. **Add the connector in Claude** once a session is online (see below): add a
|
|
71
|
+
custom connector with the address the session prints, and choose **Sign in
|
|
72
|
+
now** and **Register automatically**. A sign-in page opens; enter the
|
|
73
|
+
password.
|
|
74
|
+
|
|
75
|
+
## Run a review
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
anki-voice session
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then **wait until it prints `online at https://…/mcp`**. This often takes 10
|
|
82
|
+
minutes or more the first time in a while (see
|
|
83
|
+
[Why the wait](#why-the-wait)). Connecting earlier fails.
|
|
84
|
+
|
|
85
|
+
Once it's online, start voice mode in Claude and say something like "let's do
|
|
86
|
+
my Anki review" or "just my Spanish cards".
|
|
87
|
+
|
|
88
|
+
The session stops on its own two minutes after the review is done, or after 30
|
|
89
|
+
minutes without activity. Ctrl-C or closing the terminal stops it too. Either
|
|
90
|
+
way, the server goes offline and nothing is left on the internet.
|
|
91
|
+
|
|
92
|
+
### What you can say
|
|
93
|
+
|
|
94
|
+
| Say | What happens |
|
|
95
|
+
|---|---|
|
|
96
|
+
| your answer | Claude says whether it was right, gives the correct answer if not, and moves on |
|
|
97
|
+
| "I don't know" | Counts as wrong ("again") |
|
|
98
|
+
| your answer and "easy", or "that one was hard" | A correct answer is graded "easy" or "hard" instead of "good". Said after the grade, it changes that grade |
|
|
99
|
+
| "skip" | The card stays due in Anki; you get the next one |
|
|
100
|
+
| "don't ask me this one again" | The card's note gets the tag `anki-voice::skip` and voice reviews leave it out from now on. Remove the tag in Anki to bring it back |
|
|
101
|
+
| "undo", or "that was right!" | Takes back the last grade or skip, once |
|
|
102
|
+
| "flag this red" | Flags the card in Anki for you to look at later. Claude asks for a colour if you don't name one |
|
|
103
|
+
| "on this card, accept either order" | Claude rewrites the card's instructions (see below) |
|
|
104
|
+
| "just maths" / "everything again" | Limits the review to one topic, or lifts the limit |
|
|
105
|
+
|
|
106
|
+
Grading is strict: only a correct and complete answer is "good" (or "hard" or
|
|
107
|
+
"easy", when you ask). For nouns in languages whose articles show gender, the
|
|
108
|
+
article is part of the answer.
|
|
109
|
+
|
|
110
|
+
### Cards in other languages
|
|
111
|
+
|
|
112
|
+
Each language in `ANKI_VOICE_LANGUAGES` is a name followed by any flag emoji
|
|
113
|
+
that mark its cards. A card is in that language when its deck or note type name
|
|
114
|
+
contains the name (`Spanish`, `Study::Spanish`, `Spanish vocab`), or its front
|
|
115
|
+
or back shows one of the flags. Separate languages with commas:
|
|
116
|
+
`'Spanish 🇪🇸 🇲🇽, French 🇫🇷'`.
|
|
117
|
+
|
|
118
|
+
Claude reads these cards with its voice for that language, and speaks only
|
|
119
|
+
that language on them. Or that's how it works most of the time, it's a bit
|
|
120
|
+
finicky. If it doesn't work well, try doing a session only on that deck.
|
|
121
|
+
|
|
122
|
+
### Instructions for single cards
|
|
123
|
+
|
|
124
|
+
The `AnkiVoice instructions` field on a note tells Claude how to treat its
|
|
125
|
+
cards: what counts as an answer, how to read the prompt. Claude follows them
|
|
126
|
+
over its general rules and never reads them aloud. No card template shows the
|
|
127
|
+
field, so the cards look the same in Anki.
|
|
128
|
+
|
|
129
|
+
You can write them in Anki, or ask Claude during a review ("on this card,
|
|
130
|
+
accept either order").
|
|
131
|
+
|
|
132
|
+
### While a session runs
|
|
133
|
+
|
|
134
|
+
**Don't use Anki's window.** Two reasons:
|
|
135
|
+
|
|
136
|
+
- "undo" uses Anki's own undo, which takes back whatever changed last, even if
|
|
137
|
+
that was you in the window.
|
|
138
|
+
- Once Anki's window shows a deck or its reviewer, Anki only accepts answers
|
|
139
|
+
for the card at the top of its own queue. The session puts the window back on
|
|
140
|
+
the Decks screen when it starts, and again if Anki refuses an answer.
|
|
141
|
+
|
|
142
|
+
### Which cards come up
|
|
143
|
+
|
|
144
|
+
1. Cards in learning that are due now
|
|
145
|
+
2. Reviews due, shuffled within each due day
|
|
146
|
+
3. Cards in learning due within the next 20 minutes
|
|
147
|
+
|
|
148
|
+
New cards **do not** come up. Neither do suspended or buried cards, cards from
|
|
149
|
+
excluded decks, or cards whose front needs a picture or a sound. Claude tells
|
|
150
|
+
you at the end how many of those last were left out.
|
|
151
|
+
|
|
152
|
+
Each grade is saved in Anki the moment it's given, so a card you got wrong
|
|
153
|
+
comes back when its learning step makes it due. Anki logs each answer with the
|
|
154
|
+
time from when Claude got the card to when it was graded, less 10 seconds for
|
|
155
|
+
the speech you can't skip.
|
|
156
|
+
|
|
157
|
+
## Technical details
|
|
158
|
+
|
|
159
|
+
### Security
|
|
160
|
+
|
|
161
|
+
- **Online only during a review.** The Funnel and its public address exist only
|
|
162
|
+
while a session runs. A small guard process takes the Funnel down as soon as
|
|
163
|
+
the session is gone, even after `kill -9`. To check that nothing is
|
|
164
|
+
published:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
tailscale funnel status
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
It should say "No serve config". If only the Mac app is installed, the
|
|
171
|
+
command is `/Applications/Tailscale.app/Contents/MacOS/Tailscale`.
|
|
172
|
+
- **Password sign-in.** The server signs Claude in itself, with one password.
|
|
173
|
+
Every request without a valid token is refused.
|
|
174
|
+
- **Only Claude can sign in.** The sign-in only ever sends its result to
|
|
175
|
+
Claude's own address, so a link to the sign-in page can't be used to phish
|
|
176
|
+
the password into someone else's hands.
|
|
177
|
+
- **Tokens expire.** Claude's access lasts an hour and is renewed for up to 48
|
|
178
|
+
hours of disuse; after that, it asks for the password again.
|
|
179
|
+
- **Tokens are stored as hashes**, in `~/.local/share/anki-voice/oauth.json`
|
|
180
|
+
(readable only by you), so restarting doesn't sign Claude out. Delete that
|
|
181
|
+
file to sign everything out.
|
|
182
|
+
- **Guessing is limited.** After 10 wrong passwords in 15 minutes, sign-in is
|
|
183
|
+
refused for the rest of that window. The limit is shared by everyone, so
|
|
184
|
+
someone who finds the address can keep you from signing in for as long as
|
|
185
|
+
they keep guessing. Claude stays connected if it already is.
|
|
186
|
+
- **Card text reaches Claude as-is.** Each card's front, back and
|
|
187
|
+
`AnkiVoice instructions` field go to Claude, including text a template
|
|
188
|
+
hides, and Claude follows the instructions field over its general rules. A
|
|
189
|
+
shared deck written by someone else could use that to have Claude misgrade
|
|
190
|
+
cards or rewrite other notes' instructions. Check the instructions field of
|
|
191
|
+
shared decks before you review them by voice, or leave them out with
|
|
192
|
+
`ANKI_VOICE_EXCLUDE_DECKS`.
|
|
193
|
+
|
|
194
|
+
### Why the wait when starting a session
|
|
195
|
+
|
|
196
|
+
Tailscale publishes the server's public address in DNS only when the Funnel
|
|
197
|
+
starts, and the record often takes 10 minutes or more to appear. If anything
|
|
198
|
+
looks the address up before then, such as Claude when you try to connect, the
|
|
199
|
+
answer "no such name" is cached for up to 5 minutes. So the session waits until
|
|
200
|
+
the record exists, then until any such cached answer has expired, then until a
|
|
201
|
+
request through the public relay actually reaches the server. Only then does it
|
|
202
|
+
say it's online. After that it checks every minute and tells you if the server
|
|
203
|
+
stops answering.
|
|
204
|
+
|
|
205
|
+
### Other ways to run it
|
|
206
|
+
|
|
207
|
+
**Behind a tunnel of your own**, instead of Tailscale:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
export ANKI_VOICE_PUBLIC_URL=https://<your tunnel's address>
|
|
211
|
+
anki-voice serve
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
It gets the password the same way a session does. The server listens on
|
|
215
|
+
`127.0.0.1:8787`; point the tunnel there. The connector
|
|
216
|
+
address is `$ANKI_VOICE_PUBLIC_URL/mcp`. Unlike a session, this runs until you
|
|
217
|
+
stop it.
|
|
218
|
+
|
|
219
|
+
**For a local client** such as Claude Code, without sign-in:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
anki-voice serve --stdio
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Settings
|
|
226
|
+
|
|
227
|
+
All settings are environment variables.
|
|
228
|
+
|
|
229
|
+
| Variable | Default | What it sets |
|
|
230
|
+
|---|---|---|
|
|
231
|
+
| `ANKI_VOICE_PASSWORD_COMMAND` | | A command that prints the sign-in password |
|
|
232
|
+
| `ANKI_VOICE_PASSWORD` | | The password itself, instead of a command |
|
|
233
|
+
| `ANKI_VOICE_LANGUAGES` | none | Languages to read in their own voice, each with its flags, comma-separated |
|
|
234
|
+
| `ANKI_VOICE_EXCLUDE_DECKS` | none | Decks to leave out, with their subdecks, comma-separated |
|
|
235
|
+
| `ANKI_VOICE_IDLE_MINUTES` | `30` | Minutes without activity before a session stops |
|
|
236
|
+
| `ANKI_VOICE_LEARN_AHEAD_MINUTES` | `20` | How far ahead cards in learning come up |
|
|
237
|
+
| `ANKI_VOICE_OVERHEAD_SECONDS` | `10` | Taken off each answer time logged in Anki |
|
|
238
|
+
| `ANKI_VOICE_DRY_RUN` | | `1` to try things out: grades, instructions and flags aren't saved to Anki |
|
|
239
|
+
| `ANKI_VOICE_PORT` | `8787` | Local port of the server |
|
|
240
|
+
| `ANKI_VOICE_STATE_FILE` | `~/.local/share/anki-voice/oauth.json` | Where tokens are stored |
|
|
241
|
+
| `ANKICONNECT_URL` | `http://127.0.0.1:8765` | Where AnkiConnect listens |
|
|
242
|
+
|
|
243
|
+
### Checking AnkiConnect
|
|
244
|
+
|
|
245
|
+
anki-voice uses AnkiConnect's long-standing API (version 6) to read cards and
|
|
246
|
+
change notes, and its newer JSON-RPC API v1 (at `/api/v1`) to grade, flag and
|
|
247
|
+
undo. Older AnkiConnect releases have only the first. To check yours, with Anki
|
|
248
|
+
open:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
curl -s localhost:8765/api/v1 -d '{"jsonrpc":"2.0","id":1,"method":"operations.get","params":{"operationId":"check"}}'
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
A reply that starts with `{"jsonrpc"` (here, "Operation not found") means API v1
|
|
255
|
+
is there. anki-voice is tested with the AnkiConnect release of October 2026,
|
|
256
|
+
which needs Anki 26.8 or later.
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
## For developers
|
|
260
|
+
|
|
261
|
+
- How Claude runs a review is set by the instructions at the top of
|
|
262
|
+
`src/anki_voice/server.py`.
|
|
263
|
+
|
|
264
|
+
- You can run tests via uv: `uv run pytest`, and anki-voice from the
|
|
265
|
+
checkout with `uv run anki-voice …`.
|
|
266
|
+
- To release: set the new version in `pyproject.toml` (and `uv lock`), commit,
|
|
267
|
+
then push a tag `v<version>`. GitHub Actions runs the tests, and publishes to
|
|
268
|
+
PyPI only if the tag matches the version.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "anki-voice"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "MCP server for reviewing Anki cards by voice from the Claude app"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
keywords = ["anki", "mcp", "claude", "voice", "flashcards"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 5 - Production/Stable",
|
|
12
|
+
"Environment :: MacOS X",
|
|
13
|
+
"Operating System :: MacOS",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Education",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"mcp>=2.3,<3",
|
|
19
|
+
"httpx>=0.27",
|
|
20
|
+
"starlette>=0.40",
|
|
21
|
+
"uvicorn>=0.30",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Repository = "https://github.com/EquineOntology/anki-voice"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
anki-voice = "anki_voice.server:main"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8",
|
|
33
|
+
"anyio>=4",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
File without changes
|