Tango
Turn any YouTube video into Anki flashcards, automatically.
What it does
You give Tango a YouTube video ID. It gives you an Anki .apkg file ready to import.
YouTube video -> transcript -> spaCy NLP -> deck check -> definitions -> Anki cards
Between extraction and card creation, Tango:
- Resolves the target language from a flag or deck name and fetches the right subtitles
- Prefers manually created transcripts over auto-generated ones
- Filters vocabulary by part of speech (nouns, verbs, adjectives, adverbs)
- Checks your existing Anki deck for duplicates using a three-condition fuzzy match that handles morphologically rich languages
- Detects sentence-structured decks and skips fuzzy matching where it would not be meaningful
- Fetches example sentences, synonyms, and antonyms in the original transcript language
- Fetches the definition in your chosen output language (English by default, or native)
- Builds cards with up to two dictionary examples, a video transcript example, synonyms, and antonyms
- Creates minimal fallback cards for words with no definition found
Quick start
Prerequisites: Python 3.10+, Anki desktop, AnkiConnect add-on.
pip install tango-anki
python -m spacy download en_core_web_sm
tango run <video-id> --deck "MyDeck"
The command is tango; the package is tango-anki, because tango on PyPI
is an unrelated project. Run tango doctor at any point and it reports what
is installed, what is missing, and the command that fixes each.
Then import the generated .apkg from output/ into Anki, or say yes when it
offers to import for you.
Working on Tango itself?
git clone https://github.com/AlphaNerdFx/Tango.git
cd Tango
make all
cp .env.example .env
make run VIDEO_ID=<id> DECK="MyDeck"
Learning a language other than English? Build its offline dictionary first, or every card will read "No definition found":
make dictionary LANGUAGE=fr # any code from `tango languages`
One large download per language (a few hundred MB), then it works offline forever. See Definition coverage for why this is needed and what it gives you.
Configuration
All configuration lives in .env. Copy .env.example (or run make setup for
a guided walkthrough) and fill in what you need. Nothing here is required to
run the pipeline:
| Variable | Required | Description |
|---|---|---|
| MW_API_KEY | No | Merriam-Webster API key (free, 1000 requests/day). Improves English definitions; dictionaryapi.dev is used automatically without one |
| PROXY_HTTP_URL, PROXY_HTTPS_URL | No | Your own proxy, only needed if YouTube starts rate-limiting your IP. See Proxy notes below before using one |
| WEBSHARE_USERNAME, WEBSHARE_PASSWORD | No | Alternative to the above if you specifically use Webshare |
| ANKI_HOST | No | AnkiConnect URL. Defaults to http://localhost:8765, which is right everywhere except WSL, and WSL is detected and handled without setting this |
| LIBRETRANSLATE_URL | No | Local LibreTranslate server URL for translation mode |
Proxy notes
Most users never need a proxy. Requests come from your own residential IP by default, which is exactly the traffic YouTube doesn't aggressively block.
Webshare's free tier was tested and made things worse, not better: transcript extraction failed with repeated 429 errors through the proxy but succeeded without it, because free-tier datacenter IPs get blocked more aggressively than residential ones. This project doesn't recommend a specific provider, free or paid. If you're actually getting rate-limited, bring your own reputable proxy (a paid residential/mobile proxy you already trust, or a personal VPN).
LANGUAGE and DEF_LANG are not .env variables. They're make run command
arguments (make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr DEF_LANG=en), see
below. Setting them in .env has no effect.
WSL
One setting, and it is in Anki rather than here: AnkiConnect binds to
127.0.0.1, which WSL cannot reach. Change it to 0.0.0.0 in Anki under
Tools, Add-ons, AnkiConnect, Config.
You no longer need to set ANKI_HOST. Anki runs on the Windows side and
localhost from inside WSL is the Linux VM, so the connection is refused;
Tango notices, retries once against the Windows host it finds in the routing
table, and stays on whichever address answered. The old advice was to paste
that address into .env yourself, which worked until Windows rebooted and
reassigned it.
Setting ANKI_HOST explicitly still overrides all of this, and is never
second-guessed.
Commands
make run VIDEO_ID=<id> DECK="Deck::Name" run full pipeline
make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr specify subtitle language
make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr DEF_LANG=en English definitions
make review DECK="Deck::Name" process deferred review.json
make backlog DECK="Deck::Name" process Anki backlog
make translate-setup install translation model
make test unit tests, no network needed
make test-all full suite with integration tests
make format auto-format with black
make lint lint with ruff
make clean remove venv, output, and cache
To list all supported language codes:
tango languages
Language support
Tango resolves the target language from the deck name (a deck named "French" fetches French subtitles) or from an explicit LANGUAGE flag. The explicit flag always wins.
40 languages are supported including French, Spanish, German, Japanese, Arabic, Russian, Chinese, Korean, and more.
Example sentences, synonyms, and antonyms are always returned in the original transcript language. Definitions and grammatical class are returned in DEF_LANG if set, otherwise in the transcript language.
Translation between languages uses argostranslate locally or community LibreTranslate mirrors. Run make translate-setup to install the local model for your language pair.
Definition coverage
English is covered by Merriam-Webster and dictionaryapi.dev out of the box, at around 98%.
Every other language needs make dictionary LANGUAGE=<code>. Without it, non-English cards show "No definition found" for essentially every word. This is not a limitation of a particular language: dictionaryapi.dev returns nothing usable for any non-English language tested, measured at 0% across French, German, Spanish, Portuguese, Japanese, Russian, Korean and Chinese.
The offline dictionary is built from Wiktionary data and works with no network access once built. Measured against real generated decks:
| language | definitions | examples | synonyms | antonyms |
|---|---|---|---|---|
| French | 95% | 92% | 83% | 20% |
| German | 91% | 84% | 60% | 51% |
| Russian | 91% | 72% | 72% | 46% |
Build it for English too, since 27 August 2026. That advice used to be the opposite, and the reversal is worth knowing: Merriam-Webster still writes better definitions and is still tried first, but it is the only source English has, it allows 1000 queries a day per key, and one 1094-word video exceeds that on its own. The index is the floor under it.
Measured on a real 1094-lemma English deck, the index supplies IPA for 96.4% of words, audio for 97.0% and an example for 90.3%, all offline. Before it, English cards carried none of those whenever dictionaryapi.dev was unreachable, which it was for the whole day this was measured. See docs/ADR-011-english-offline-index.md.
The antonym column above is what the Wiktionary index alone gives. Antonyms have their own optional index, built once for every language at the same time:
make antonyms
It is a 498 MB download that is streamed rather than stored, leaving 4.3 MB on disk. Measured end to end on real decks, it takes French from 19.7% to 34.8%, German from 56.2% to 60.3% and Russian from 47.8% to 48.8%. The gain is concentrated in French because both sources extract the same Wiktionary edition with different tools, and they disagree about which words carry an antonym. The French dictionary index has one for 15,045 words and ConceptNet has 12,376, overlapping only partly; German already holds 30,616 against ConceptNet's 3,547, so it gains less. Without it, every card is exactly what it was.
How duplicate detection works
Tango compares each extracted lemma against your existing deck's card fronts using three conditions that must all pass.
WRatio above 90: word already in deck, skipped. WRatio between 60 and 90, token sort ratio above 50, and length ratio above 0.6: possible duplicate, you decide at the prompt. Anything else: new word, definition fetched and card created.
The three-condition filter prevents false positives in morphologically rich languages. "commencer" no longer incorrectly matches "comme" even though WRatio scores it at 90.
Sentence-structured decks skip fuzzy matching entirely and use exact match only.
Card fields
Each card contains:
- Word (front)
- Class (part of speech, written in the same language as the Definition)
- Definition (in DEF_LANG or native language)
- 1st Example Sentence (from dictionary, in original language)
- 2nd Example Sentence (from dictionary, in original language)
- Example from Youtube Video (transcript sentence)
- Synonyms (in original language)
- Antonyms (in original language)
- VideoID and Source (where the card came from)
- IPA (pronunciation transcription, in the original language)
- Pronunciation (the recording itself, embedded so it plays in the card)
Everything describing the word stays in the transcript language, including the recording. A German word defined in French is still pronounced in German.
Fields are appended, never reordered. Indices 0-11 are what every
already-imported card is bound to. Adding one is a notetype schema change:
Tango aligns your collection's notetype before importing, and Anki will ask
for one full sync afterwards. See CHANGELOG.md for the v0.5.0 migration.
Project structure
tango/
├── src/pipeline/
│ ├── config.py config and environment variables
│ ├── language.py language resolution and BCP-47 mapping
│ ├── translation.py argostranslate integration and mirror fallback
│ ├── transcript.py YouTube transcript extraction
│ ├── nlp.py spaCy vocabulary extraction
│ ├── deck.py AnkiConnect duplicate detection
│ ├── definition.py definition fetching and caching
│ ├── cards.py Anki card and package generation
│ ├── state.py SQLite state management
│ └── __main__.py CLI entry point
├── tests/
├── docs/
├── pyproject.toml
└── Makefile
Roadmap
Goals are tracked per release tag in ROADMAP.md, which also records what v1.0.0 freezes and what is deliberately out of scope.
v1.0.0 is a finished CLI — installable from a package, running on Windows, macOS and Linux, on low-end and high-end hardware alike.
| tag | goal |
|---|---|
| v0.5.0 | pronunciation on cards, and a notetype that merges (current) |
| v0.5.1 | pronunciation for every language, starting with English |
| v0.6.0 | card quality |
| v0.7.0 | the command line as a product |
| v0.8.0 | runs on any operating system |
| v0.9.0 | runs on modest hardware |
| v0.10.0 | packaged and installable |
| v1.0.0 | a finished CLI |
A browser extension, a web or desktop app, and distribution to other language ecosystems are out of scope for 1.0.0 — plausibly a separate project sharing a common premise. See ROADMAP.md §4.
Release history is in CHANGELOG.md.
Requirements
- Python 3.10+
- Anki desktop with AnkiConnect add-on (code: 2055492159)
- Merriam-Webster API key (free tier)
- spaCy model: python -m spacy download en_core_web_sm
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tango_anki-0.8.0.tar.gz.
File metadata
- Download URL: tango_anki-0.8.0.tar.gz
- Upload date:
- Size: 244.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4dbe1dbc19ddeccb4b32860d00845132c8c8a58af3d4b70a7befe64f3c248cbf
|
|
| MD5 |
d77b7c405893bb21cf068a917c311a9b
|
|
| BLAKE2b-256 |
3ded6971e583745a79577ed287ccab48c6a5eafc1d875ebab9bb72b4ca98c5a2
|
File details
Details for the file tango_anki-0.8.0-py3-none-any.whl.
File metadata
- Download URL: tango_anki-0.8.0-py3-none-any.whl
- Upload date:
- Size: 136.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
315626d3abb9131dc5d56781f670c332c32390463951d91de5b4c60ae0ee0b4f
|
|
| MD5 |
804d7e5d018b24b73c076e7ecc24a88b
|
|
| BLAKE2b-256 |
203b3e3b2072ab83d486c920bde51d26c64a133968e3ad5f753d17ed75dc0b52
|