Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
66 changes: 35 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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`.
Expand Down
36 changes: 36 additions & 0 deletions docs/audio.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions examples/elements.csv
Original file line number Diff line number Diff line change
@@ -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
13 changes: 10 additions & 3 deletions examples/general-knowledge.csv
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions examples/http-status.csv
Original file line number Diff line number Diff line change
@@ -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
26 changes: 26 additions & 0 deletions examples/nato-phonetic.csv
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions tests/tests.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Minimal assert-style harness: no framework, just `make test`.
#include <dirent.h>
#include <sys/stat.h>

#include <clocale>
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();
Expand Down
Loading