Class notes in, Anki cards out
Raw class notes go into a folder; an LLM agent turns them into a structured dictionary, sentences and Anki cards under a written specification. A Python script computes which cards each word requires, validates every change, synthesizes speech with Azure and drives Anki desktop through AnkiConnect. Items are ordered by HSK 3.0 level, and each batch returns a review to the learner. The system is MIT, the accumulated content CC BY.
There are four ways into the Mandarin dictionary on this site, and none of them is a link: a pin behind my portrait on the About page, a line in the footer, three letters typed on any page, and a command in the palette for those who know what to ask for. I hid them on purpose and I still stand by it, so for everyone with better things to do, here is the dictionary, and here is the repository that builds it.
Day one26 · sep 25Read noteThe deck's repository opens with one line of intent, a practical Chinese deck for Anki, and the pipeline with an inbox, digests and audio arrives the next day. The first batch of class notes went through it that weekend.
Scope creep26 · sep 29Read noteThe page grew. It shows the cards themselves, drawn from the same HTML Anki gets, lets you download the deck, and plots how the deck grew day by day from the repository's history. It reads the repository directly, so a push there shows up here within a couple of minutes, and it can examine you: a round of the deck's own cards, typed answers checked the way Anki checks them, and whatever you bury comes back at the end of the round.
Scope creep, again26 · oct 19Read noteThe page now also shows how I study, read from my Anki history: what Anki asked of me each day and what I did with it, how much of what I have seen I would recall today, which buttons I press. The notebook was rebuilt: open a word, then one of its characters, then a piece of that character, and a column on the left keeps the path so you can walk back. It also sets exams, and the generative one is ruled: it writes a prompt for any LLM to make a reading test, with the vocabulary picked from the log (words I have actually studied), so the model never chooses it. Have a look. And learn some Chinese while you are at it, for geopolitical reasons.
This week the deck crossed HSK 1, the first of the nine levels the new HSK uses to measure Mandarin, with about 500 words on it1. I know that is a small number, and I also know the machinery that got me there is rather less small: a folder where I drop class notes in whatever state they come out, an LLM agent that turns them into a dictionary and a set of Anki cards, a Python script that refuses to let either of us cut corners, and a review that comes back to me after every batch telling me what I missed. The machinery came late, though, and the language came first, by a route I did not plan.
1Ministry of Education of the PRC, Chinese Proficiency Grading Standards for International Chinese Language Education (GF 0025-2021), 2021, summarised in Hanyu Shuiping Kaoshi, Wikipedia, which gives the nine levels and 500 words for HSK 1.
How I ended up with a tutor and a pipeline
It started with a YouTube video about why China makes most of what the rest of us use (this one, in Spanish): I watched it for the industry and came out of it with a language to learn, which is not how these things usually go. The choice between German and Mandarin took longer than it should have, because German wins on every neutral measure: a shared alphabet, cognates, a B2 in under two years from Spanish. What tipped it was machine translation. It already covers the documents and the emails, which is most of what German would have given me at a tech company, whereas in China the part that translation cannot touch (the lunch, the side comments, the chat groups, walking into a factory and reading the room) is exactly the part that matters most, and it gets more valuable as the tools get better.
With the language settled, the question became how to learn it. I shortlisted two schools in Barcelona, a large institute with a business Mandarin track and a small one with groups capped at seven, and ended up with neither, because for Mandarin a private tutor wins on the one thing that matters most at the start, which is tones. A tone gets fixed when someone hears you get it wrong and stops you on the spot, and in a group of seven that happens to you perhaps five minutes an hour1. One to one, the whole hour is yours, the pace follows you instead of the slowest student, and (this is where the pipeline comes in) the tutor can work from what the review says I am missing, so the schools can wait until I need the business track.
1A sixty-minute class split seven ways, minus the teacher's own speaking time, leaves each student somewhere between five and eight minutes of talking.
Before we start
What happens between one class and the next is the deck's job, and two ideas explain why a deck of flashcards is worth this much plumbing. Both are well established, and both are optional reading if you already use Anki.
SpacingRetrievalskip
Spacing
Memory decays on a curve that Hermann Ebbinghaus measured on himself in 1885, and every review that happens just before you would have forgotten flattens that curve a little more1. That is why spaced practice beats the same number of repetitions crammed together, an effect that holds across hundreds of studies, and why a scheduler that shows each card again at a growing interval is the cheapest way to exploit it2.
1Ebbinghaus, Memory: a contribution to experimental psychology, 1885 (English translation by Ruger and Bussenius, 1913), whose chapter 7 measures his own retention of nonsense syllables from twenty minutes to 31 days. 2The standard reference is Cepeda et al., Distributed practice in verbal recall tasks: a review and quantitative synthesis, Psychological Bulletin 132, 2006, a meta-analysis of 317 experiments from 184 articles.
Retrieval
Pulling an answer out of memory strengthens it more than reading it again. Students who were tested on a passage remembered more of it a week later than students who reread it for the same time, even though the rereaders felt better prepared1. This is why every card in the deck asks me to produce something (type the pinyin, type the hanzi, mark the tones, say the sentence aloud) instead of showing me a word and asking whether I know it.
1Roediger and Karpicke, Test-enhanced learning: taking memory tests improves long-term retention, Psychological Science 17, 2006.
Anki in one paragraph
Anki is that scheduler in practice, a free flashcard program that decides, card by card, when you should see each one again, based on how well you answered last time. A note holds the content, a card is one question generated from it, a deck is a folder of cards, and a preset holds the daily limits. Decks are built in the desktop app, AnkiWeb is a free sync service, and the phone apps (AnkiDroid, AnkiMobile) sync against it, so a card edited on the computer shows up on the phone after one tap. The piece that makes the rest of this article possible is AnkiConnect, an add-on that exposes Anki as an HTTP API on localhost:8765 while the desktop app is open, which means a script can import a deck, move cards between decks, set limits and trigger a sync without anyone touching the interface.
From a folder of notes to a deck
Everything around that API is a repository with numbered folders, one per stage, and a single Python script, anki.py, that does everything computable, while the agent (I use Claude Code, but any coding agent that reads files works) does everything that needs judgement.
One batch as a loop: my notes go in, the agent writes and the script checks, Azure gives it a voice, Anki and the phone get the deck, and the review I answer starts the next batch
The review goes back into the inbox, which closes the loop, since my answers to it become part of the next batch. The folders, in order:
1-inboxwhere I write: phone notes, transcribed photos of paper, half-finished lists, where the only thing that helps is a date on top. Processed notes are archived untouched in history/, one folder per batch.
2-digestsone log per batch, written by the agent and never rewritten: what went in, what did not and why, what it corrected in my notes, what it reorganized.
3-datathe single source of truth, one YAML file per theme: words, expressions, characters, pronunciation rules, groups of easily confused items, sentences, cards and the generated audio. Only the agent edits it.
4-notebookthe same data rendered as Markdown for reading, regenerated every batch.
5-outputthe compiled .apkg and a public dictionary.json.
A batch is whatever sits in the inbox when I say process the inbox. There is no fixed rhythm: I ask when there is enough to process, ideally right after a class so the review lands before the next one, and the structure is never frozen either: every batch the agent revisits themes and groups and reorganizes them when the new material asks for it. Cards keep a permanent ID through all of it, so moving one never costs its review history.
Now it listens26 · sep 30Read noteThe inbox also takes a recording of a class now. Whisper large-v3 transcribes it on my own machine, on the CPU and for free (about 40 minutes for a 73-minute class), and the batch reads the transcript as context. Large-v3 because it is the most accurate Whisper for Mandarin; the small one, kept only to detect the language, mistook short words like 对 or 可以. A voiceprint tells the teacher from me, and her lines weigh more, since mine are a learner's. Whatever Whisper turned from her English into Chinese is caught and dropped. The recording and the transcript never leave the computer.
An agent that sleeps between batches
That sentence, process the inbox, is also the only thing that wakes the agent up, which goes against the fashion. The fashionable shape for an agentic system right now is one that never stops (a loop on a schedule, a memory store, tools calling tools, a dashboard to watch it think), and I felt the pull until I counted what this problem needs, which is a handful of runs a month, each started by a person who has just come back from class.
So the agent here is awake only while it works, and everything that makes it behave lives in files placed where it will look. AGENTS.md at the root is the first thing any coding agent opens, and it points to the specification (docs/design.md, how the system works) and to the learner (docs/goal.md, who it works for); every folder carries a README.md saying what lives there and who may touch it; and the numbered folder names give it the order of the work before it has read a line. The directory is the interface, and a well-placed paragraph of instructions does the job a runtime would do in a more ambitious design, with nothing to host, nothing to monitor and nothing running at three in the morning.
The whole thing took a couple of days with Opus 5.5 at the keyboard and Azure giving it a voice (I said as much at the time), and most of those days went into the rules, since the code followed from them. That is also why I expect it to age well: the rails are plain text and a validator, so when the next model arrives it reads the same files and writes better cards under the same checks, and the deck improves without my touching the system.
A specification in six files
Everything the agent does comes from six Markdown files in the repository, public like the rest, about 185 KB between them, nearly eight times the length of this article. Each one says at the top what belongs in it and what does not, and the agent is told to keep it that way when it edits them.
AGENTS.mdthe entry point, about a page: what to read first, what only the agent may edit, and the hard rules (keep every ID, never touch my other decks in Anki, add files to a commit one by one, never push unless I ask).
design.mdhow the system works: the twelve steps of a batch, the data schema, which cards each word requires, the audio settings, the sixty or so rules check enforces and the order of the review.
goal.mdthe learner: who, what for, what goes in to be said and what only to be understood, and how the review should talk to me (direct, every Chinese word with its pinyin beside it).
decisions.mdwhy each rule exists, append-only, with a revoked entry marked as revoked instead of deleted.
hypotheses.mdwhat I believe works for learning, each belief with what would settle it; the review reports on them.
owner.mdmy own setup: Azure, the quirks of syncing my phone, daily limits, maintenance every four batches.
Changing a rule follows an order of its own. First an entry in decisions.md saying why, then the rule rewritten in the present tense in design.md or goal.md with no trace of the old wording, and, when it can be checked, a test in check. The specification therefore always reads as the system it describes today, while the reasons pile up in one file I can read back when a rule starts to feel arbitrary.
Two kinds of rule do most of the work. The first tells the agent when to stop: before every push it waits until I confirm the phone is synced, and anything ambiguous in my notes (a meaning, a reading, whether I already know a word) goes into the review as a question instead of a guess. The second separates what may move from what may not: themes, groups and each word's use are reorganised every batch, while a rule changes only in the retrospective every four batches, or when I ask for it in capitals or between brackets in my answers.
The model writes, the script checks
Of those rules, the one I would keep if I threw everything else away is that anything that can be computed is decided by code, from rules written in a specification, and the model only works on the difference the code hands it. Left alone, a model will happily write four cards for a word you only need to recognise and none for a word you need to say, which is why every entry carries a use, a small ladder where each rung includes the one below it:
readrecognise the hanzia card that shows the character hearunderstand it spokenadds a listening card sayproduce itadds typed production, tones and speaking
From use, the part of speech and the pronunciation, anki.py plan computes the cards every entry requires and lists what is missing, and scaffold writes the standard ones in a fixed format, so the agent only drafts the few that need real prose. Then check refuses the batch if any required card is missing, if two cards test the same thing, if a word I want to say never appears inside a sentence, if an ID changed since the last batch, or if a pinyin reading disagrees with the dictionary without a stated reason. Closing a batch commits and tags it (lote-001, lote-002), which gives check a baseline to compare against and gives me a way back from any mistake, and a GitHub Actions workflow runs check and the tests again on every push.
Also new26 · sep 29Read noteThe deck now teaches sentence structures too, 23 so far, written with typed gaps such as {S} + 很 + {Adj}, each gap a coloured box and each structure with cards of its own. Every word also carries its grammatical category, which the notebook on this site filters by.
This is what plan printed after the first batch, in the Spanish the whole deck runs in. No required card was missing, but twelve words I wanted to be able to say appeared in no sentence yet (no aparece en ninguna frase), so all twelve went straight into the review as homework:
BASH Copy
$ python anki.py plan w.qing 请 (ósmosis): no aparece en ninguna frase w.dajia 大家 (ósmosis): no aparece en ninguna frase w.zhongwen 中文 (ósmosis): no aparece en ninguna frase w.shangwu 上午 (ósmosis): no aparece en ninguna frase
0 tarjeta(s) exigida(s) que faltan; 12 entrada(s) sin frase.
Beyond the checks, two details did more for the deck than their size suggests. The typed answer accepts pinyin with tone marks, pinyin with digits (ni3 hao3) or hanzi from a phone keyboard, and ignores spacing and punctuation, which meant writing the comparison in a few lines of JavaScript inside the card template; and the back of each card shows the IPA transcription, the pinyin traps a Spanish speaker falls into (the b in bà is really an unaspirated p) and the family of the word when it has one, so 他, 她 and 它 always turn up together. This is one on my phone, a listening card I had just answered:
The typed answer, checked against pinyin with tone marks or digits, or hanziPronunciation traps, an example sentence, and the next intervals Anki offers: one, two, six or fourteen days
Speech, cached
Every answer and every sentence on those cards also has audio, synthesized with Azure's neural text-to-speech (the voice is zh-CN-YunyangNeural), slowed down to 70% for sentences and 60% for single words, with a short silence in front because some players swallow the first syllable. Files are named after a hash of the text and the synthesis settings and indexed by the exact Chinese string, so a word is paid for once no matter how many cards use it, and changing the voice regenerates everything cleanly. The paid tier costs cents per batch, and although the free tier would have done for me alone, Microsoft keeps commercial use of the audio for the paid tiers, the licence I publish under allows commercial use, and the whole point was to publish it1.
1Microsoft Q&A, Can I use Azure Text-to-Speech to generate mp3 for commercial use?, 28 November 2024, where Microsoft's answer rules out the free F0 tier for commercial use and allows it on pay-as-you-go.
Driving Anki from outside
With the data, the cards and the audio in place, anki.py push compiles the deck with genanki, opens Anki desktop if it is not running, waits for AnkiConnect and then does in a few seconds what I used to do with the mouse: imports the package, forces the card templates to update, orders the new cards so the basics come first, moves each card to the subdeck of its level and theme, sets the daily limits in a preset of its own and syncs with AnkiWeb, from where the phone picks it up at the next sync.
That convenience needs a leash, because my Anki collection holds other decks I did not build, and an HTTP API that can delete decks is a loaded gun. Every destructive call is scoped to this deck's name, the limits live in a preset this deck owns so the shared default is never touched, a subdeck is removed only after checking it is empty, and deleting more than ten orphaned cards requires an explicit --force. I learned to insist on this the honest way: early on, a cleanup call went out without listing its targets first, and it only did no damage because there was nothing to damage. Those guards now have tests of their own, run against a simulated AnkiConnect with foreign decks in the collection. It also means nothing gets corrected inside Anki, since every push overwrites the deck from the data and a fix made in the app would simply vanish.
Levels, a goal file and a review
The subdecks that push fills follow the levels of HSK 3.0, the 2021 grading standard behind the Chinese proficiency test, which orders everything in the deck: levels 1 to 6, plus a seventh bucket for levels 7 to 9, which share one vocabulary list1. A word on the official list takes its official level, from an MIT-licensed copy of the list kept in the repository, while anything else (a lot of what a person actually says is not on any exam) gets a level the agent estimates, with the reason written down. A sentence takes the level of its hardest word, so it never smuggles vocabulary from later on, and the deck is split by level first and theme second, so I study one level at a time.
1Wikipedia, Hanyu Shuiping Kaoshi, cited above, which tabulates the 2021 standard level by level.
Levels say how hard a word is, while what the agent keeps for a given learner lives in one file, docs/goal.md: who is learning, what for (in my case speaking, work, living in China, in roughly that order), what goes in to be said, what only to be understood and what stays out. The agent reads it before every batch and applies it as the first filter on every decision. It works as a swappable prompt module, because the rest of the specification describes how the system works and holds no personal preference, so pointing the pipeline at someone else is a matter of rewriting one file.
Both of them meet in the review, which is the part I did not expect to like this much. It runs to about fifteen sections in a fixed order, and only the first is compulsory: Sine qua non, at most seven concrete questions the next batch cannot start without, each with a link to its detail. Then comes how I am doing (my level and the distance to the next, what my tutor has been covering and whether it fits what I want, how I have been studying and one concrete piece of advice), what changed in the deck, a short lesson that corrects my homework, how each learning hypothesis is holding up, the cards I fail most with a proposal for each, the daily load, and the gaps, thematic ones (I could say morning and evening but not afternoon), basics of my level I have not met and characters with a story worth knowing. After that, an account of the batch itself: what went in with something worth digging into, what is still pending, what came back from the attic, what stayed out and why, and what I have to confirm. Every fourth batch adds a maintenance task in Anki and a retrospective of the process, and the review ends with a free space for my own notes.
The review of a batch, block by block. Only the first column has to be answered before the next batch; the agent reads every answer and the deck changes with them.
Under every point there is a line that starts with > for my answer. An empty line keeps things as they are, sácalo (take it out) rescues something from the attic, and whatever I write in capitals or between brackets counts as an instruction about the system itself; the next batch applies each answer and reports in its log what it did with it. My tutor sees some of those gaps in class the following week, so between the two of them I rarely have to decide what to study next.
A slice of the first review, from its section of gaps and proposals (Huecos y propuestas), in Spanish as I wrote it, with the line under each point left empty for my answer:
NúmerosSolo tienes 八. Del 一 al 十, más 几 y 多少, abren fechas, horas, precios y edades.
Mi respuesta…
VerbosTienes 喝, 买 y 看, pero no 吃, 做, 说 ni 来.
Mi respuesta…
A template with my notes inside
Since the goal file is the only personal part of the system, the repository is really two things sharing a folder, and the licenses say so. The system (the script, the specification, the tests) is MIT: fork it, empty the data folders, rewrite the goal file, change the deck name and it is yours. The content (my notes, the dictionary, the logs, the audio) is CC BY, so you can use it for anything, commercial included, as long as you name where it came from. The public dictionary is an export with an explicit list of publishable fields, and it is what powers the page on this site. The page opens on a copy bundled with the site, pinned to a batch tag, then asks GitHub for the commit main points to and reads 5-output/dictionary.json at that exact commit through jsDelivr, so a push to the deck shows up there without touching the site. Reading at a commit instead of at main is deliberate: jsDelivr caches a branch on its own, and once the page kept showing an old batch for hours after a new one was pushed.
Where each step runs. The commands on the laptop in the order a batch uses them, the services each one calls, and how the page on this site finds the newest export.
The deck itself is not on AnkiWeb's shared decks yet. It is still small and I am still early in Mandarin, but I wanted the whole thing piped before the content grew, and there is a reminder in my calendar to publish it there; until then, anyone who wants it can build it from the repository.
HSK 2 adds 772 words in the 2021 standard (the exam syllabus published in November 2025 asks for fewer, 500 in total by the end of HSK 2), which to the pipeline are just more batches in the inbox1.
1Wikipedia, Hanyu Shuiping Kaoshi, cited above, which gives 772 new words for HSK 2 in the 2021 standard and 500 cumulative in the exam syllabus released on 15 November 2025.