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();