Releases: AlphaNerdFx/Tango
Release list
Runs on any operating system
Runs on any operating system, and now there is evidence for the claim.
CI on Linux, macOS and Windows
The rung's own words were "because should work is not evidence". Development
happens on WSL2, so until this job existed every statement about the other
two platforms was an assertion.
Linux covers the Python range, macOS and Windows run one version each,
because what they test is the operating system rather than the interpreter.
Five jobs, not nine.
It found a real bug on its first run. macOS passed. Windows failed five
tests in three classes, and only one of them could reach a user.
The real bug: an interrupted build left a file behind
wiktdata.py and antonyms.py both deleted the partial .building file in
their error handler while the SQLite connection was still open. POSIX unlinks
an open file happily and frees the inode when the last handle closes. Windows
refuses with WinError 32, "the process cannot access the file because it is
being used by another process", so the partial index survived for the next
run to inherit.
Both now close before unlinking.
The other four failures were tests that had only ever run on Linux: three
compared paths against POSIX literals, when str(Path) uses the native
separator, and one patched Path.resolve to a /mnt/c path, which on
Windows is a drive-relative WindowsPath the WSL regex cannot match. They
were testing the platform rather than the code.
A WSL user who clones to ~ can see what went wrong
Anki runs on the Windows side and can only open paths Windows can see.
/mnt/<drive> paths are translated; anything under /home or /tmp cannot
be, because it has no Windows equivalent. That happens to anyone who clones
to their home directory, which is the more natural place to clone.
What they saw was AnkiConnect's own file-not-found, which says nothing about
why a file that plainly exists cannot be found. It now names the cause and
both ways out: import by hand, or point OUTPUT_DIR somewhere under /mnt.
Gated on which host answered rather than on being under WSL. A WSL user can
run Anki inside WSL through WSLg, and there localhost is the right address
and POSIX paths are exactly what AnkiConnect wants. Blocking those would have
broken a setup that already works.
The CLI is the primary path
GNU make is not a reasonable requirement on Windows, and make check-os
correctly refuses to run there without Git Bash, WSL or Cygwin. Every
user-facing target already had a tango equivalent, so what changed is the
documentation: it now leads with the command a user actually has.
Upgrading
pip install --upgrade tango-ankiNothing to do to your collection. The notetype is unchanged.
Full detail in CHANGELOG.md.
Nothing fails without saying why
Nothing fails without saying why.
Every release before this one fixed failures as they were met: seven messages
in v0.7.0, eight stale flag names in v0.8.0, a port 8765 traceback in v0.8.2.
Each was found by someone tripping over it or reading the code. CLAUDE.md has
always required that no expected failure produces a traceback, and that was
enforced by noticing. This release went looking instead.
Seven things it found
An unexpected error is a message, not a traceback. Anything unanticipated
printed a Python traceback at the user, which is not actionable and reads as
the tool breaking rather than as a case it does not handle. It now says what
went wrong, that it is a bug rather than something you did, and where to
report it. TANGO_DEBUG=1 gives the traceback back.
A corrupt or unwritable database is a typed error. Reproduced rather than
imagined: a truncated file gives "file is not a database", an unwritable path
gives "unable to open database file". Each now names its own fix, whether
that is deleting the file, checking permissions, or closing another run.
The .apkg write is guarded. The last step was the one place a bare
OSError could reach you, and the worst: transcript fetched, every definition
looked up and paid for, audio downloaded, then a full disk takes the run with
it. The message now says how many cards were built and lost.
TangoError. Twenty typed exceptions across nine modules had nothing in
common, so the entry point could not tell a failure someone wrote a message
for from a genuine bug. Without it the first fix would have made the second
worse: a corrupt database would have been reported to you as a bug in Tango.
tango doctor names settings in .env that nothing reads. A setting that
does nothing is a failure with no message. Found in a real .env:
SPACY_MODEL=en_core_web_sm, which looks exactly like it chooses the model
and is read by nothing, and API_DELAY, a leftover. The model comes from the
language code; the knob that exists is SPACY_MODEL_SIZE_OVERRIDE.
Two implementations of WSL detection, both reading /proc/version.
A dead exception class, defined and documented and never raised.
Also
src/ now holds only the importable package. It also held nine icons for a
UI that does not exist, never imported and never packaged. Checked against
the PyPA guidance and six well-known projects before moving anything: src/
is "the code that is intended to be importable", psf/requests has exactly
src/requests/, and none of the six keeps a loose image at its repository
root.
The Dockerfile now ships in the sdist, so you can build the image without
cloning. SECURITY.md stopped naming a version: it said v0.4.x was the
latest supported while the project was on v0.8.2.
Upgrading
pip install --upgrade tango-ankiNothing to do to your collection. The notetype is unchanged.
Full detail in CHANGELOG.md.
An install that looks after itself
An install that looks after itself. Three items cut from v0.8.0 so the name
could be claimed, plus one gap a reader found in the code.
A missing spaCy model is offered, not just reported
A fresh pip install tango-anki got as far as fetching the transcript and
then stopped with "spaCy model not found. Run: ...". The message was correct
and the timing was not: the work was already spent, and the user was sent
away to run a second command before seeing anything work at all.
The check now runs before the transcript is fetched, where it costs nothing,
and on a terminal it offers to do the download:
[warn] The spaCy model for 'ca' (ca_core_news_sm) is not installed.
[info] It is a one-off download of a few tens of MB.
Download ca_core_news_sm now? [Y/n]:
Answering yes installs it and the run carries straight on. Answering no, or
running with the output piped, prints tango install-model <lang> and exits
1 rather than prompting into something that cannot answer.
The dictionary index is deliberately still manual. It is a
several-hundred-MB download per language, which is not something to start
without being asked.
tango uninstall
pip uninstall tango-anki removes about 130 KB of Python and leaves
everything that takes space. Measured on the development machine: 1.1 GB of
dictionary indexes, 82 MB of packages, 46 MB of cached audio, a 4.9 MB
definition cache. pip only owns what it installed, so nothing else could
offer to remove the rest.
1.1 GB Dictionary and antonym indexes
rebuild with 'tango build-dictionary <lang>'
80.8 MB Generated .apkg packages
your decks, if you have not imported them yet
4.9 MB Definition cache and run history
expensive to rebuild: it is every definition ever fetched
--dry-run reports and stops. --yes skips the prompt. Piped with no
--yes it reports and deletes nothing, because the alternative is a script
silently destroying a cache that took hours of API calls to build. The paths
come from your config, so a redirected DICT_DIR or DB_PATH is reported
rather than the defaults.
A Dockerfile
docker build -t tango .
docker run --rm -v "$PWD/out:/data/output" tango run <id> --deck "French"663 MB, English model baked in, all state in a /data volume, non-root so
the files it writes are deletable by the host user. Built and run before
release rather than written and hoped for.
Something else on port 8765
AnkiConnect answers on 8765 and nothing guarantees AnkiConnect is what is
there. A different service on that port, or a stale ANKI_HOST, got as far
as a successful HTTP request and then died on response.json() with
"Expecting value: line 1 column 1 (char 0)".
Three shapes are now typed errors naming the address, the port and
ANKI_HOST: an answer that is not JSON, an HTTP error status, and JSON with
no result field. Deliberately not "Anki is not running", because the port
answered, so that is the one cause already ruled out.
Found by a reader looking at the code, not by a failure.
Upgrading
pip install --upgrade tango-ankiNothing to do to your collection. The notetype is unchanged.
Full detail in CHANGELOG.md.
Documentation for the people who can now install it
A documentation release, published the same day as v0.8.0 and for a reason
worth writing down.
The README is also the PyPI description, and every command in it assumed a
cloned repository: make run VIDEO_ID=... DECK=..., make dictionary,
make antonyms, make translate-setup. None of that exists after
pip install tango-anki. The shop window for a package published that
morning was telling its first visitors to run a build tool they did not have.
PyPI freezes a release's description at upload time and will not let it be
edited, so correcting the published page costs a release. That is what this
one is.
What changed
Every command in the README is now a tango subcommand. The Makefile
equivalents are kept, in a collapsed section, for people working in a clone.
The version badge was stuck at v0.5.3, five releases behind, because it
was hardcoded. It now reads live from PyPI and from the GitHub releases API,
next to a new badge for the package. Neither can go stale again.
The roadmap table was wrong about the plan, not just the version. It
still showed the ordering from before 27 August 2026, when packaging sat at
v0.10.0 behind cross-platform support. It now shows the real sequence, where
the project actually is, and the known rough edges.
A .pyc had been committed. .gitignore listed tests/__pycache__ and
src/pipeline/__pycache__ by name, so scripts/__pycache__ was never
covered. Replaced with __pycache__/ and *.py[cod], which catch every
directory including ones that do not exist yet.
.env.example documents the PyPI publishing keys. PYPI_USER must be
the literal string __token__, not an account name. An account name paired
with a pypi- token is rejected with a 403 that does not say why, which is
what tripped this project's first upload.
Upgrading
Nothing to do. No code changed; pip install --upgrade tango-anki only
updates the metadata and the description.
The rung previously numbered v0.8.1, first-run downloads, a Dockerfile and a
real uninstall, is now v0.8.2.
Full detail in CHANGELOG.md.
Packaged and installable
Tango is on PyPI.
pip install tango-anki
python -m spacy download en_core_web_sm
tango run <video-id> --deck "French"The package is tango-anki because tango on PyPI is an unrelated project.
The command you type is still tango; the two names do not have to match,
and only one of them gets typed more than once.
Until now the only way in was cloning the repository. Card quality,
portability and disk footprint all matter more once someone can run the
thing, and less than nothing before.
Also in this release
tango --version. The first question about any bug report is which build
produced it, and until v0.7.0 there was no installed command to ask.
WSL reaches Anki without being told where it is. ANKI_HOST defaults to
http://localhost:8765, which is right on macOS, native Linux, native
Windows and WSL2 with mirrored networking. On WSL2's default NAT network it
is wrong: Anki runs on the Windows side. The old advice was to find the
gateway with ip route and paste it into .env, which worked until Windows
rebooted and reassigned it.
A refused connection is now retried once against the Windows host read from
the routing table, and the address that answered is kept for the rest of the
run. It is a retry rather than a different default on purpose: defaulting to
the gateway under WSL would break mirrored networking, where localhost is
correct. An explicit ANKI_HOST is never second-guessed.
nltk is an extra. It supplements the synonym and antonym fields and
nothing else, so an install without it is a working install with thinner
synonyms. pip install tango-anki[wordnet] for it.
Eight messages named flags that v0.7.0 deleted. A missing translation
model said python -m pipeline --install-translation, an AnkiConnect error
said --doctor, a stale index said --build-dictionary. None of those
flags exist. Naming the fix was v0.7.0's own goal, and these named a fix that
could only fail. Two of them had tests, which passed throughout because they
asserted the flag spelling rather than the intent.
What the install actually costs
Measured in a clean virtualenv: 334 MB across 58 packages, no torch and
no translation stack. 236 MB of that, 74%, is spacy and the numeric stack it
needs, which is the floor while spacy is the NLP engine.
Cut to v0.8.1
First-run model downloads, a Dockerfile, and an uninstall that removes the
indexes. None stops anyone installing or running the tool, and an unclaimed
distribution name can be taken by anyone.
Upgrading
Nothing to do to your collection; the notetype is unchanged.
Full detail in CHANGELOG.md.
The command line as a product
The first release aimed at someone who did not write it. Nothing here changes
a card or asks anything of an existing collection. It changes what you type,
what the run tells you while it works, and what a failure says when it stops.
A real command, with subcommands
tango run <video-id> --deck "French"
tango review --deck "French"
tango doctor
tango languages
Plus backlog, setup, install-model, install-translation,
build-dictionary and build-antonyms. The old surface put every mode
behind a boolean flag on one parser, so --help listed sixteen options
without indicating that --review and --video-id are different programs,
and nothing stopped you passing both.
There is also a console entry point for the first time. The project had no
[project.scripts] at all, so even an editable install gave you
python -m pipeline rather than a verb.
This is a breaking CLI change, made deliberately now, while the interface
has no installed users, rather than after v0.8.0 publishes it.
A long run says what it is doing
Pacing Merriam-Webster turned the definition phase into minutes of silence on
a large English video, and silence is indistinguishable from a hang. Every
phase now reports its own elapsed time, and the definition phase carries a
progress line with a completion estimate.
The line redraws in place on a terminal, so a thousand-word run stays one line
rather than a thousand. Redirected anywhere else it prints one line per decile
instead: carriage returns in a log file are noise, and tango run ... > run.log
is a normal thing to do here.
Failures name the fix
Seven messages stopped the run while stating only the outcome, leaving you to
find the flag in --help or guess that one exists. Now an already-processed
video names --force and shows the exact command; a missing translation model
names --install-translation de:en and the alternative of dropping
--def-lang; an AnkiConnect error carries the action that failed; an
AnkiConnect timeout names the modal dialog that causes it nearly every time;
and an empty transcript says the video's captions are empty instead of naming
a private function and a private dict key.
Upgrading
Nothing to do to your collection. The notetype is unchanged. If you have
scripts calling the old flag surface, they need rewriting to the subcommands
above; tango --help lists all ten.
Full detail in CHANGELOG.md.
Card quality
Card quality: what is in the fields, rather than which fields exist. Five items, no migration. The notetype is unchanged, so an existing collection needs nothing done to it and no review history is touched.
Antonyms have an offline source of their own
The weakest field on the card by a wide margin: 19.7% on a real 1054-lemma French deck, against 98.6% for definitions. make antonyms builds a 4.3 MB index from ConceptNet covering 22 languages at once, and the field is filled from it when nothing else can.
| deck | before | after |
|---|---|---|
| French, 1054 lemmas | 19.7% | 34.8% |
| German, 406 lemmas | 56.2% | 60.3% |
| Russian, 701 lemmas | 47.8% | 48.8% |
The asymmetry is the finding. ConceptNet's antonyms are Wiktionary's own, re-extracted by a different tool: kaikki runs wiktextract, ConceptNet ran wikiparsec, and on the same edition the two disagree about which words carry an antonym. The French dictionary index has one for 15,045 words and ConceptNet has 12,376, comparable in size and overlapping only partly, so the union is much larger than either. German already holds 30,616 against ConceptNet's 3,547 and Russian 23,747 against 1,857, so there it mostly hands back what is already present. See docs/ADR-010-conceptnet-antonyms.md.
Entirely optional: without the index every card is exactly what it was, and --doctor reports it as absent rather than missing.
Filler sounds no longer become cards
Ah, Bah, Euh, Tss: 3.4% of one real French run, and the cards that look broken. A per-language stoplist rather than a part-of-speech rule, because Bonsoir is also INTJ and is worth learning.
Inflection pointers resolve to a real definition
Russian was the last language still letting a pointer gloss reach a card as its definition.
The run names the words that got no definition
Instead of only counting them. On a real 406-card German run that is 28 words, most of them transcript damage or names you would delete on sight. They are deliberately not filtered: three signals were measured and none separates them from real vocabulary, so the run names them and you decide.
The definition cache key carries both languages
It is now lemma::source::target::pos. A cross-language run writes rows holding the transcript language's examples, and keyed only by the definition language those rows were readable by a run in a different language that shares a spelling. Measured on a real 5408-row cache, 265 rows were already in that state and had simply never been hit. Existing rows are set aside as definitions_v0 rather than re-keyed by guesswork, and the cache refills as words are met again.
Also in this release
The translation install no longer pulls 4.5 GB of CUDA that no machine without an NVIDIA card can use. make translate-setup installs the CPU-only torch build and repairs an environment that already has the CUDA one. Measured here, .tangovenv went from 5.9 GB to 2.2 GB.
Full changelog: https://github.com/AlphaNerdFx/Tango/blob/main/CHANGELOG.md
Part of speech in the learner's language
The part of speech is written in the learner's language.
Changed
-
Class is localised. wiktextract normalises the part of speech to an
English tag whichever Wiktionary edition an index was built from, so a
German card readnounand a French one readadj. Cards now read
Substantivandadjectif.The label follows the definition, so it never disagrees with the text
beside it: with--def-lang fra German word readsnom, and without it
the same word readsSubstantiv. Constraint 3.3 already allowedClass
to change language, and this is the reason it does.Labels for de, fr, ru, es, it, pt and en are in
language.POS_LABELS.
A language with no table falls back to English, which still expandsadj
intoadjective, and a tag with no entry is shown unchanged rather than
dropped. Adding a language is one row. -
Existing cards keep whatever they were imported with. Re-import to update
them; nothing needs migrating and no review history is affected.
Audio plays inside the card
Pronunciation audio plays inside the card instead of linking out.
Added
-
Embedded audio. Recordings are downloaded when the package is built and
shipped inside the.apkg, so the Pronunciation field is now
[sound:...]and plays in place. A link opened a browser, which is not
reviewing; embedded audio also works on AnkiDroid and AnkiMobile and keeps
working when the source is down.ADR-009 rejected this on size grounds and the estimate was wrong. Measured
on real Commons files — De-Haus 16 KB, De-Spaziergang 30 KB — a 240-card
German deck costs about 5 MB, not the "tens of megabytes" assumed.
Wikimedia already serves them as MP3, so nothing is converted. -
media.py, with an on-disk cache (MEDIA_DIR, defaultmedia/) so a word
met in a second video is not downloaded twice. -
A link fallback per card. When a download fails the card keeps the
linked URL rather than losing the audio entirely. This is routine, not
defensive padding: dictionaryapi.dev served one word's audio and returned
502 for another in the same minute. -
Paced downloads. Audio requests are spread across the run at
MEDIA_RATE_LIMITper second (default 1.25) after a burst of
MEDIA_BURST, and a429is retried for the period the server asks for.Without this the feature above barely worked.
upload.wikimedia.org
rate-limits per IP — about ten requests, then429withRetry-After: 11
— so an 8-worker pool drained the allowance in under a second. A real
406-card German run embedded 13 recordings and linked the other 364,
with no error and a valid package. Going sequential did not help; only
spacing the requests did. ARCHITECTURE.md 8.35. -
Progress output while audio downloads. Paced downloading of a few
hundred files takes minutes, and the CLI previously sat silent at
"Building Anki package...". The embedded-versus-linked count was already
being logged atINFO, below the defaultWARNINGlevel, so the one
number that revealed the bug above was written and discarded.
Fixed
- The documented non-interactive recipe crashed.
echo "s" | make run
supplies one line; the queued-word prompt consumed it and the import prompt
then hitEOFError, ending the run with a traceback after the package had
been written. All prompts now treat exhausted stdin as "no input" and take
a safe default. CLAUDE.md 4.4.
Known issues
- Images are ADR-009 phase 3 and deliberately not built: sampling
measured 2 of 5 usable, withlaufenreturning a coin from the town of
Laufen. They need a relevance gate before they are worth having.
Pronunciation describes the word on the card
Pronunciation now describes the word on the card, in every mode and in
English. No migration: the fields added in v0.5.0 are unchanged, and this
only alters what goes into them.
Fixed
-
Cross-language mode put the wrong word's pronunciation on the card.
With--def-lang,ipaandaudio_urlwere read from the translated
word's index entry, so a German video with--def-lang frshowedHaus
carrying maison's\me.zɔ̃\and a French recording. Examples, synonyms
and antonyms were already gated against exactly this; pronunciation had
been added beside the gate without being inside it.Pronunciation is now resolved once, by
_resolve_pronunciation(lemma, language), independent of which source supplied the definition. No
definition branch touches it. SeeARCHITECTURE.md8.34.
Added
- English pronunciation. dictionaryapi.dev returns real IPA (
/haʊs/)
and a complete audio URL in aphoneticsblock, on a call the pipeline
already made and never parsed. English cards now carry pronunciation even
when Merriam-Webster supplied the definition. - Pronunciation on English fallback cards, through the same resolver.
Changed
CLAUDE.md§3.3 is now stated as a question — does this field describe
the word shown? — rather than a list of three field names. Written as a
list, it was violated three times, each by someone adding a fourth thing
beside the gate.