From 385a350edeed57f0fb9164408a050696775fdab0 Mon Sep 17 00:00:00 2001 From: Aduneer <249940941+Aduneer@users.noreply.github.com> Date: Wed, 19 Aug 2026 21:03:54 +0200 Subject: [PATCH] Four more example decks, and move piper setup out of the README Groundwork before v0.3.0, in three parts. EXAMPLE DECKS http-status.csv (18 codes, tagged by class), elements.csv (19 symbols, weighted towards the ones from Latin names, which are the only ones worth a flashcard), and nato-phonetic.csv (all 26). general-knowledge.csv grew from 5 cards to 12 real ones; "What's 2 + 2 - 1?" was filler next to every other deck here and is gone. nato-phonetic.csv turns out to be the first shipped deck that exercises the single-key guard. Reviewed reversed its answers are single letters, so a card answered "Q" would collide with quit and one answered "A" with audio. Checked against the real binary rather than assumed: reversed, the Q card's legend withholds "[q] end session" and the A card's withholds "[a] play audio", and both answer correctly. NO PICTURES ON THE EXISTING LANGUAGE DECKS, deliberately. spanish.csv and japanese.csv run foreign to English, so a picture would show the meaning, which on those cards is the answer. That is the rule colores.csv exists to demonstrate, and following it here means not doing the obvious thing. NO SHIPPED AUDIO either, for the reason --generate-audio exists: recordings are rendered locally, not committed. What was missing was the command, so the README now carries it, with voice names taken from voices that actually exist rather than guessed -- ja_JA-hi_fi_captain-medium is not the ja_JP the naming convention suggests. README The piper install-and-troubleshoot material moves to docs/audio.md. It was the one section that was reference rather than tour: how to install a text-to-speech engine, where its voices live, and the extra package Japanese needs. The README did not get shorter -- 653 lines to 657. The setup appendix left and a deck table and the audio recipe arrived. That is a better balance rather than a smaller file, and the bigger split (Syncing and Development out too, roughly halving it) is still available if it is ever wanted. A TEST FOR CONTENT The example decks were entirely uncovered: they are data, so a missing comma does not fail to build, it quietly drops a card -- or ships one whose answer is empty and can therefore never be got right. Every examples/*.csv is now loaded and checked: every non-blank line became a card, no question or answer is blank or normalises to nothing, and every picture a deck names is actually present. Verified the check can fail, rather than trusting that it passes: a card with no answer trips the line count, and a repointed image path trips the picture check. 1184 unit checks and 47 golden cases, green on gcc and clang, optimised and sanitised, and clean in an env -i clean room. --- CHANGELOG.md | 13 +++++++ README.md | 66 ++++++++++++++++++---------------- docs/audio.md | 36 +++++++++++++++++++ examples/elements.csv | 19 ++++++++++ examples/general-knowledge.csv | 13 +++++-- examples/http-status.csv | 18 ++++++++++ examples/nato-phonetic.csv | 26 ++++++++++++++ tests/tests.cpp | 52 +++++++++++++++++++++++++++ 8 files changed, 209 insertions(+), 34 deletions(-) create mode 100644 docs/audio.md create mode 100644 examples/elements.csv create mode 100644 examples/http-status.csv create mode 100644 examples/nato-phonetic.csv diff --git a/CHANGELOG.md b/CHANGELOG.md index bed157f..7da96ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,21 @@ detail, which is where the reasoning lives. file, or one that is not really an image, quietly becomes a card without a picture rather than an error. +- **Four more example decks, and a fifth rewritten.** `http-status.csv` (18 + codes, tagged by class), `elements.csv` (19 symbols, including the ones from + Latin names that make the deck worth having), `nato-phonetic.csv` (all 26), + and `colores.csv` with pictures. `general-knowledge.csv` grew from 5 cards to + 12 real ones. + + `nato-phonetic.csv` is also the first shipped deck that exercises the + single-key guard: reviewed reversed, its answers are single letters, so `Q` + and `A` would otherwise collide with the quit and audio keys. + ### Changed +- **Piper setup moved to `docs/audio.md`,** leaving the README with what the + feature *is* rather than how to install a text-to-speech engine. + - **CI builds under the sanitizers and with `-Werror`,** as a four-way matrix of both compilers against an optimised and a sanitised build. Every pull request so far was checked under `-fsanitize=address,undefined` by hand and came back diff --git a/README.md b/README.md index c32b73a..974f10d 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,18 @@ tracked by git, so studying never shows up as a source change. ### Starter Decks A new deck starts empty. To fill it, choose **5. Import flashcards** and give it -any file from `examples/` — programming languages, tooling and human languages: +any file from `examples/`: -```bash -ls examples/ -``` +| Deck | | +| --- | --- | +| `cpp.csv`, `python.csv` | Language features and syntax | +| `git.csv`, `linux-cli.csv` | Commands worth having in your fingers | +| `http-status.csv` | Status codes, tagged by class | +| `elements.csv` | Chemical symbols, including the ones from Latin names | +| `nato-phonetic.csv` | The phonetic alphabet, all 26 | +| `spanish.csv`, `japanese.csv` | Vocabulary, foreign → English | +| `colores.csv` | Colours, English → Spanish, **with pictures** | +| `general-knowledge.csv` | A bit of everything | Imported cards arrive in Box 1 and are due immediately. Most of the examples are plain `question,answer,tags` records, the shortest form of the deck format below, @@ -70,6 +77,23 @@ than one answer, which is worth copying: `mañana` really does mean both *tomorrow* and *morning*, and `git init` should not be marked wrong because you typed `init`. +`nato-phonetic.csv` is worth reviewing **reversed** (`r`) at least once: the +answers become single letters, so `Q` and `A` would collide with the quit and +audio keys. They are withheld exactly when a card could accept them, which is +easier to believe once you have watched it happen. + +**Adding audio to a language deck** takes one command — recordings are rendered +locally rather than shipped, so nothing large lives in this repo: + +```bash +cp examples/spanish.csv ~/spanish.txt # copy first: studying writes to the deck +FlashTerm ~/spanish.txt --generate-audio # lists the voices you have +FlashTerm ~/spanish.txt --generate-audio --voice es_ES-davefx-medium +``` + +`ja_JA-hi_fi_captain-medium` does the same for `japanese.csv`, though Japanese +needs one extra package first — see [docs/audio.md](docs/audio.md). + `colores.csv` is the exception, and the one to copy if you want [pictures](#images): each card carries a colour swatch from `examples/images/`. It also runs **English → Spanish**, against the direction of every other language deck here, @@ -307,35 +331,15 @@ already has, whatever it calls its output flag: FLASHTERM_TTS_RENDER="espeak-ng -v fr --stdin -w {out}" ``` -#### About piper - -[Piper](https://github.com/OHF-voice/piper1-gpl) is a neural text-to-speech -system from the Home Assistant authors. It runs offline, needs no GPU, and its -voices are dramatically better than `espeak-ng` for language learning — which is -the whole point of hearing a card. Voices are about 60 MB each and cover 30-odd -languages; list them all with -`python3 -m piper.download_voices --help` (using the interpreter FlashTerm -printed for you). - -FlashTerm does not bundle, link against, or require piper. It runs whatever -command you put in `FLASHTERM_TTS_RENDER` as a separate process, so piper's -GPL-3.0 licence applies to piper and FlashTerm stays MIT — and swapping in a -different engine is a one-line change, not a fork. - -#### Japanese +### Setting up piper -Piper's Japanese voice needs one extra package, because it phonemizes through -`pyopenjtalk` rather than through espeak like the others, and `pipx install -piper-tts` does not bring it along: - -```bash -pipx inject piper-tts pyopenjtalk -``` +Piper is a neural text-to-speech system that runs offline and sounds +dramatically better than `espeak-ng` — which is the whole point of hearing a +card. FlashTerm does not bundle it, link against it, or require it. -The first card is then slow — it downloads a pronunciation dictionary of about -23 MB once — and after that Japanese works like any other language, kanji -included. Without it, rendering fails with a `ModuleNotFoundError`; -FlashTerm recognises that particular failure and tells you the command above. +**[docs/audio.md](docs/audio.md)** covers installing it, what the voices are, +where they live, why there is no default voice, and the one extra package +Japanese needs. One general caveat: a recording only ever matches the question it was rendered from. Edit a card's question and its audio is stale until you `--force`. diff --git a/docs/audio.md b/docs/audio.md new file mode 100644 index 0000000..64b5ba3 --- /dev/null +++ b/docs/audio.md @@ -0,0 +1,36 @@ +# Audio setup + +Everything FlashTerm needs to speak a card, in more detail than the +[README](../README.md) wants to carry. Nothing here is required: with none of +it installed, review simply never offers the `a` key and `--generate-audio` +tells you what to do about it. + +## About piper + +[Piper](https://github.com/OHF-voice/piper1-gpl) is a neural text-to-speech +system from the Home Assistant authors. It runs offline, needs no GPU, and its +voices are dramatically better than `espeak-ng` for language learning — which is +the whole point of hearing a card. Voices are about 60 MB each and cover 30-odd +languages; list them all with +`python3 -m piper.download_voices --help` (using the interpreter FlashTerm +printed for you). + +FlashTerm does not bundle, link against, or require piper. It runs whatever +command you put in `FLASHTERM_TTS_RENDER` as a separate process, so piper's +GPL-3.0 licence applies to piper and FlashTerm stays MIT — and swapping in a +different engine is a one-line change, not a fork. + +## Japanese + +Piper's Japanese voice needs one extra package, because it phonemizes through +`pyopenjtalk` rather than through espeak like the others, and `pipx install +piper-tts` does not bring it along: + +```bash +pipx inject piper-tts pyopenjtalk +``` + +The first card is then slow — it downloads a pronunciation dictionary of about +23 MB once — and after that Japanese works like any other language, kanji +included. Without it, rendering fails with a `ModuleNotFoundError`; +FlashTerm recognises that particular failure and tells you the command above. diff --git a/examples/elements.csv b/examples/elements.csv new file mode 100644 index 0000000..cf65dd1 --- /dev/null +++ b/examples/elements.csv @@ -0,0 +1,19 @@ +H,Hydrogen,chemistry;elements;symbols +He,Helium,chemistry;elements;symbols +C,Carbon,chemistry;elements;symbols +N,Nitrogen,chemistry;elements;symbols +O,Oxygen,chemistry;elements;symbols +Na,Sodium,chemistry;elements;symbols;latin +Mg,Magnesium,chemistry;elements;symbols +K,Potassium,chemistry;elements;symbols;latin +Ca,Calcium,chemistry;elements;symbols +Fe,Iron,chemistry;elements;symbols;latin +Cu,Copper,chemistry;elements;symbols;latin +Zn,Zinc,chemistry;elements;symbols +Ag,Silver,chemistry;elements;symbols;latin +Sn,Tin,chemistry;elements;symbols;latin +Sb,Antimony,chemistry;elements;symbols;latin +W,Tungsten,chemistry;elements;symbols;latin +Au,Gold,chemistry;elements;symbols;latin +Hg,Mercury,chemistry;elements;symbols;latin +Pb,Lead,chemistry;elements;symbols;latin diff --git a/examples/general-knowledge.csv b/examples/general-knowledge.csv index 8ecce2f..f0dfcb5 100644 --- a/examples/general-knowledge.csv +++ b/examples/general-knowledge.csv @@ -1,5 +1,12 @@ What is the capital of France?,Paris,geography;europe -Who developed the theory of relativity?,Albert Einstein|Einstein,science;physicist +What is the capital of Australia?,Canberra,geography;oceania +Which planet is closest to the Sun?,Mercury,science;astronomy Largest planet in our solar system?,Jupiter,science;astronomy -What's 2 + 2 - 1?,3,quickmaths -Python list comprehension syntax?,[x for x in iterable],programming;python +Largest ocean on Earth?,Pacific|Pacific Ocean,geography;oceans +Who developed the theory of relativity?,Albert Einstein|Einstein,science;physicist +Who wrote Hamlet?,Shakespeare|William Shakespeare,literature;plays +In which year did the Second World War end?,1945,history;20th-century +How many bones are in the adult human body?,206,science;anatomy +Which gas do plants absorb from the air?,Carbon dioxide|CO2,science;biology +What is the smallest prime number?,2,maths;numbers +What is the currency of Japan?,Yen|Japanese yen,geography;currency diff --git a/examples/http-status.csv b/examples/http-status.csv new file mode 100644 index 0000000..0003d41 --- /dev/null +++ b/examples/http-status.csv @@ -0,0 +1,18 @@ +200,OK,http;status;success +201,Created,http;status;success +204,No Content,http;status;success +301,Moved Permanently,http;status;redirect +302,Found,http;status;redirect +304,Not Modified,http;status;redirect +400,Bad Request,http;status;client-error +401,Unauthorized,http;status;client-error +403,Forbidden,http;status;client-error +404,Not Found,http;status;client-error +405,Method Not Allowed,http;status;client-error +409,Conflict,http;status;client-error +422,Unprocessable Content|Unprocessable Entity,http;status;client-error +429,Too Many Requests,http;status;client-error +500,Internal Server Error,http;status;server-error +502,Bad Gateway,http;status;server-error +503,Service Unavailable,http;status;server-error +504,Gateway Timeout,http;status;server-error diff --git a/examples/nato-phonetic.csv b/examples/nato-phonetic.csv new file mode 100644 index 0000000..20447a8 --- /dev/null +++ b/examples/nato-phonetic.csv @@ -0,0 +1,26 @@ +A,Alfa|Alpha,nato;phonetic;alphabet +B,Bravo,nato;phonetic;alphabet +C,Charlie,nato;phonetic;alphabet +D,Delta,nato;phonetic;alphabet +E,Echo,nato;phonetic;alphabet +F,Foxtrot,nato;phonetic;alphabet +G,Golf,nato;phonetic;alphabet +H,Hotel,nato;phonetic;alphabet +I,India,nato;phonetic;alphabet +J,Juliett|Juliet,nato;phonetic;alphabet +K,Kilo,nato;phonetic;alphabet +L,Lima,nato;phonetic;alphabet +M,Mike,nato;phonetic;alphabet +N,November,nato;phonetic;alphabet +O,Oscar,nato;phonetic;alphabet +P,Papa,nato;phonetic;alphabet +Q,Quebec,nato;phonetic;alphabet +R,Romeo,nato;phonetic;alphabet +S,Sierra,nato;phonetic;alphabet +T,Tango,nato;phonetic;alphabet +U,Uniform,nato;phonetic;alphabet +V,Victor,nato;phonetic;alphabet +W,Whiskey|Whisky,nato;phonetic;alphabet +X,X-ray|Xray,nato;phonetic;alphabet +Y,Yankee,nato;phonetic;alphabet +Z,Zulu,nato;phonetic;alphabet diff --git a/tests/tests.cpp b/tests/tests.cpp index 34b5eea..4e4b8c2 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -1,4 +1,5 @@ // Minimal assert-style harness: no framework, just `make test`. +#include #include #include @@ -1867,6 +1868,56 @@ void test_card_image_column() { std::string("decks/images/perro.png")); } +void test_shipped_example_decks() { + // The example decks are content, not code, and a missing comma in one would + // not fail to build -- it would quietly drop a card, or worse, ship a card + // whose answer is empty and can therefore never be got right. Nothing else + // in the suite ever opens these files. + DIR* dir = opendir("examples"); + if (dir == nullptr) { + // Run from somewhere other than the repo root. Say so rather than passing + // silently, which is what a vacuous check looks like from the outside. + std::cout << "SKIP: examples/ not found; run tests from the repo root\n"; + return; + } + + int decks = 0; + while (const dirent* entry = readdir(dir)) { + const std::string name = entry->d_name; + if (name.size() < 5 || name.substr(name.size() - 4) != ".csv") continue; + ++decks; + + Deck deck("examples/" + name); + EXPECT_TRUE(deck.load()); + + // Every non-blank line has to have become a card: a deck that loses one to + // a stray quote still loads perfectly happily with fewer cards in it. + std::ifstream file("examples/" + name); + std::string line; + std::size_t lines = 0; + while (std::getline(file, line)) { + if (!trim(line).empty()) ++lines; + } + EXPECT_EQ(deck.size(), lines); + + for (const auto& card : deck.cards()) { + EXPECT_TRUE(!trim(card.question).empty()); + EXPECT_TRUE(!trim(card.answer).empty()); + // An answer of only alternative separators accepts nothing at all. + EXPECT_TRUE(!normalize_answer(card.answer).empty()); + // A picture named by a shipped deck has to actually be there, or the + // example teaches the feature by failing to demonstrate it. + if (!card.image.empty()) { + std::ifstream picture(deck.image_path(card), std::ios::binary); + EXPECT_TRUE(picture.good()); + } + } + } + closedir(dir); + // Guards against the whole check passing because it found nothing to do. + EXPECT_TRUE(decks >= 8); +} + void test_summarize() { const int today_days = today(); @@ -2237,6 +2288,7 @@ int main() { test_image_protocol(); test_image_terminal_detection(); test_card_image_column(); + test_shipped_example_decks(); test_summarize(); test_card_ids(); test_wrap();