tango-anki 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. tango_anki-0.8.0/LICENSE +21 -0
  2. tango_anki-0.8.0/PKG-INFO +340 -0
  3. tango_anki-0.8.0/README.md +290 -0
  4. tango_anki-0.8.0/pyproject.toml +228 -0
  5. tango_anki-0.8.0/setup.cfg +4 -0
  6. tango_anki-0.8.0/src/pipeline/__init__.py +17 -0
  7. tango_anki-0.8.0/src/pipeline/__main__.py +1479 -0
  8. tango_anki-0.8.0/src/pipeline/antonyms.py +418 -0
  9. tango_anki-0.8.0/src/pipeline/cards.py +835 -0
  10. tango_anki-0.8.0/src/pipeline/config.py +400 -0
  11. tango_anki-0.8.0/src/pipeline/deck.py +873 -0
  12. tango_anki-0.8.0/src/pipeline/definition.py +1876 -0
  13. tango_anki-0.8.0/src/pipeline/language.py +780 -0
  14. tango_anki-0.8.0/src/pipeline/media.py +274 -0
  15. tango_anki-0.8.0/src/pipeline/nlp.py +549 -0
  16. tango_anki-0.8.0/src/pipeline/state.py +349 -0
  17. tango_anki-0.8.0/src/pipeline/transcript.py +219 -0
  18. tango_anki-0.8.0/src/pipeline/translation.py +814 -0
  19. tango_anki-0.8.0/src/pipeline/wiktdata.py +650 -0
  20. tango_anki-0.8.0/src/tango_anki.egg-info/PKG-INFO +340 -0
  21. tango_anki-0.8.0/src/tango_anki.egg-info/SOURCES.txt +37 -0
  22. tango_anki-0.8.0/src/tango_anki.egg-info/dependency_links.txt +1 -0
  23. tango_anki-0.8.0/src/tango_anki.egg-info/entry_points.txt +2 -0
  24. tango_anki-0.8.0/src/tango_anki.egg-info/requires.txt +28 -0
  25. tango_anki-0.8.0/src/tango_anki.egg-info/top_level.txt +2 -0
  26. tango_anki-0.8.0/tests/test_antonyms.py +264 -0
  27. tango_anki-0.8.0/tests/test_cards.py +826 -0
  28. tango_anki-0.8.0/tests/test_config.py +163 -0
  29. tango_anki-0.8.0/tests/test_deck.py +1146 -0
  30. tango_anki-0.8.0/tests/test_definition.py +2504 -0
  31. tango_anki-0.8.0/tests/test_hard_constraints.py +496 -0
  32. tango_anki-0.8.0/tests/test_language.py +559 -0
  33. tango_anki-0.8.0/tests/test_main.py +1385 -0
  34. tango_anki-0.8.0/tests/test_media.py +275 -0
  35. tango_anki-0.8.0/tests/test_nlp.py +902 -0
  36. tango_anki-0.8.0/tests/test_state.py +457 -0
  37. tango_anki-0.8.0/tests/test_transcript.py +220 -0
  38. tango_anki-0.8.0/tests/test_translation.py +654 -0
  39. tango_anki-0.8.0/tests/test_wiktdata.py +717 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Youssef Larbi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,340 @@
1
+ Metadata-Version: 2.4
2
+ Name: tango-anki
3
+ Version: 0.8.0
4
+ Summary: Turn YouTube transcripts into Anki flashcards for language learners
5
+ Author: Youssef Larbi
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/AlphaNerdFx/Tango
8
+ Project-URL: Repository, https://github.com/AlphaNerdFx/Tango
9
+ Project-URL: Changelog, https://github.com/AlphaNerdFx/Tango/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/AlphaNerdFx/Tango/issues
11
+ Keywords: anki,flashcards,language-learning,youtube,vocabulary,spaced-repetition
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: End Users/Desktop
15
+ Classifier: Natural Language :: English
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Education
22
+ Classifier: Topic :: Text Processing :: Linguistic
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: youtube-transcript-api<2.0,>=1.2.4
27
+ Requires-Dist: spacy<4.0,>=3.8
28
+ Requires-Dist: rapidfuzz<4.0,>=3.0
29
+ Requires-Dist: requests<3.0,>=2.28
30
+ Requires-Dist: genanki<1.0,>=0.13
31
+ Requires-Dist: python-dotenv<2.0,>=1.0
32
+ Requires-Dist: typer<1.0,>=0.12
33
+ Provides-Extra: wordnet
34
+ Requires-Dist: nltk>=3.8; extra == "wordnet"
35
+ Provides-Extra: translation
36
+ Requires-Dist: argostranslate>=1.9; extra == "translation"
37
+ Requires-Dist: libretranslate>=1.6; extra == "translation"
38
+ Provides-Extra: dev
39
+ Requires-Dist: tango-anki[wordnet]; extra == "dev"
40
+ Requires-Dist: build>=1.0; extra == "dev"
41
+ Requires-Dist: twine>=5.0; extra == "dev"
42
+ Requires-Dist: pytest<10.0,>=8.0; extra == "dev"
43
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
44
+ Requires-Dist: black>=24.0; extra == "dev"
45
+ Requires-Dist: ruff>=0.4; extra == "dev"
46
+ Requires-Dist: mypy>=1.9; extra == "dev"
47
+ Requires-Dist: types-requests>=2.28; extra == "dev"
48
+ Requires-Dist: tomli>=2.0; python_version < "3.11" and extra == "dev"
49
+ Dynamic: license-file
50
+
51
+ # Tango
52
+
53
+ [![CI](https://github.com/AlphaNerdFx/Tango/actions/workflows/ci.yml/badge.svg)](https://github.com/AlphaNerdFx/Tango/actions/workflows/ci.yml)
54
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
55
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
56
+ [![Version](https://img.shields.io/badge/version-v0.5.3-orange)](https://github.com/AlphaNerdFx/Tango/releases/tag/v0.5.3)
57
+
58
+ Turn any YouTube video into Anki flashcards, automatically.
59
+
60
+ ---
61
+
62
+ ## What it does
63
+
64
+ You give Tango a YouTube video ID. It gives you an Anki .apkg file ready to import.
65
+
66
+ ```
67
+ YouTube video -> transcript -> spaCy NLP -> deck check -> definitions -> Anki cards
68
+ ```
69
+
70
+ Between extraction and card creation, Tango:
71
+
72
+ - Resolves the target language from a flag or deck name and fetches the right subtitles
73
+ - Prefers manually created transcripts over auto-generated ones
74
+ - Filters vocabulary by part of speech (nouns, verbs, adjectives, adverbs)
75
+ - Checks your existing Anki deck for duplicates using a three-condition fuzzy match that handles morphologically rich languages
76
+ - Detects sentence-structured decks and skips fuzzy matching where it would not be meaningful
77
+ - Fetches example sentences, synonyms, and antonyms in the original transcript language
78
+ - Fetches the definition in your chosen output language (English by default, or native)
79
+ - Builds cards with up to two dictionary examples, a video transcript example, synonyms, and antonyms
80
+ - Creates minimal fallback cards for words with no definition found
81
+
82
+ ---
83
+
84
+ ## Quick start
85
+
86
+ Prerequisites: Python 3.10+, [Anki](https://apps.ankiweb.net/) desktop, [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on.
87
+
88
+ ```bash
89
+ pip install tango-anki
90
+ python -m spacy download en_core_web_sm
91
+ tango run <video-id> --deck "MyDeck"
92
+ ```
93
+
94
+ The command is `tango`; the package is `tango-anki`, because `tango` on PyPI
95
+ is an unrelated project. Run `tango doctor` at any point and it reports what
96
+ is installed, what is missing, and the command that fixes each.
97
+
98
+ Then import the generated .apkg from `output/` into Anki, or say yes when it
99
+ offers to import for you.
100
+
101
+ <details>
102
+ <summary>Working on Tango itself?</summary>
103
+
104
+ ```bash
105
+ git clone https://github.com/AlphaNerdFx/Tango.git
106
+ cd Tango
107
+ make all
108
+ cp .env.example .env
109
+ make run VIDEO_ID=<id> DECK="MyDeck"
110
+ ```
111
+
112
+ </details>
113
+
114
+ **Learning a language other than English?** Build its offline dictionary
115
+ first, or every card will read "No definition found":
116
+
117
+ ```bash
118
+ make dictionary LANGUAGE=fr # any code from `tango languages`
119
+ ```
120
+
121
+ One large download per language (a few hundred MB), then it works offline
122
+ forever. See [Definition coverage](#definition-coverage) for why this is
123
+ needed and what it gives you.
124
+
125
+ ---
126
+
127
+ ## Configuration
128
+
129
+ All configuration lives in `.env`. Copy `.env.example` (or run `make setup` for
130
+ a guided walkthrough) and fill in what you need. Nothing here is required to
131
+ run the pipeline:
132
+
133
+ | Variable | Required | Description |
134
+ |---|---|---|
135
+ | MW_API_KEY | No | [Merriam-Webster API key](https://dictionaryapi.com/register/index.htm) (free, 1000 requests/day). Improves English definitions; dictionaryapi.dev is used automatically without one |
136
+ | 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 |
137
+ | WEBSHARE_USERNAME, WEBSHARE_PASSWORD | No | Alternative to the above if you specifically use Webshare |
138
+ | 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 |
139
+ | LIBRETRANSLATE_URL | No | Local LibreTranslate server URL for translation mode |
140
+
141
+ ### Proxy notes
142
+
143
+ Most users never need a proxy. Requests come from your own residential IP by
144
+ default, which is exactly the traffic YouTube doesn't aggressively block.
145
+
146
+ Webshare's free tier was tested and made things worse, not better: transcript
147
+ extraction failed with repeated 429 errors through the proxy but succeeded
148
+ without it, because free-tier datacenter IPs get blocked more aggressively
149
+ than residential ones. This project doesn't recommend a specific provider,
150
+ free or paid. If you're actually getting rate-limited, bring your own
151
+ reputable proxy (a paid residential/mobile proxy you already trust, or a
152
+ personal VPN).
153
+
154
+ `LANGUAGE` and `DEF_LANG` are not `.env` variables. They're `make run` command
155
+ arguments (`make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr DEF_LANG=en`), see
156
+ below. Setting them in `.env` has no effect.
157
+
158
+ ### WSL
159
+
160
+ One setting, and it is in Anki rather than here: AnkiConnect binds to
161
+ 127.0.0.1, which WSL cannot reach. Change it to `0.0.0.0` in Anki under
162
+ Tools, Add-ons, AnkiConnect, Config.
163
+
164
+ You no longer need to set `ANKI_HOST`. Anki runs on the Windows side and
165
+ `localhost` from inside WSL is the Linux VM, so the connection is refused;
166
+ Tango notices, retries once against the Windows host it finds in the routing
167
+ table, and stays on whichever address answered. The old advice was to paste
168
+ that address into `.env` yourself, which worked until Windows rebooted and
169
+ reassigned it.
170
+
171
+ Setting `ANKI_HOST` explicitly still overrides all of this, and is never
172
+ second-guessed.
173
+
174
+ ---
175
+
176
+ ## Commands
177
+
178
+ ```bash
179
+ make run VIDEO_ID=<id> DECK="Deck::Name" run full pipeline
180
+ make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr specify subtitle language
181
+ make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr DEF_LANG=en English definitions
182
+ make review DECK="Deck::Name" process deferred review.json
183
+ make backlog DECK="Deck::Name" process Anki backlog
184
+ make translate-setup install translation model
185
+ make test unit tests, no network needed
186
+ make test-all full suite with integration tests
187
+ make format auto-format with black
188
+ make lint lint with ruff
189
+ make clean remove venv, output, and cache
190
+ ```
191
+
192
+ To list all supported language codes:
193
+
194
+ ```bash
195
+ tango languages
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Language support
201
+
202
+ 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.
203
+
204
+ 40 languages are supported including French, Spanish, German, Japanese, Arabic, Russian, Chinese, Korean, and more.
205
+
206
+ 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.
207
+
208
+ Translation between languages uses argostranslate locally or community LibreTranslate mirrors. Run make translate-setup to install the local model for your language pair.
209
+
210
+ ### Definition coverage
211
+
212
+ English is covered by Merriam-Webster and dictionaryapi.dev out of the box, at around 98%.
213
+
214
+ **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.
215
+
216
+ The offline dictionary is built from Wiktionary data and works with no network access once built. Measured against real generated decks:
217
+
218
+ | language | definitions | examples | synonyms | antonyms |
219
+ |---|---|---|---|---|
220
+ | French | 95% | 92% | 83% | 20% |
221
+ | German | 91% | 84% | 60% | 51% |
222
+ | Russian | 91% | 72% | 72% | 46% |
223
+
224
+ 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.
225
+
226
+ 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`.
227
+
228
+ 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:
229
+
230
+ ```bash
231
+ make antonyms
232
+ ```
233
+
234
+ 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.
235
+
236
+ ---
237
+
238
+ ## How duplicate detection works
239
+
240
+ Tango compares each extracted lemma against your existing deck's card fronts using three conditions that must all pass.
241
+
242
+ WRatio above 90: word already in deck, skipped.
243
+ WRatio between 60 and 90, token sort ratio above 50, and length ratio above 0.6: possible duplicate, you decide at the prompt.
244
+ Anything else: new word, definition fetched and card created.
245
+
246
+ The three-condition filter prevents false positives in morphologically rich languages. "commencer" no longer incorrectly matches "comme" even though WRatio scores it at 90.
247
+
248
+ Sentence-structured decks skip fuzzy matching entirely and use exact match only.
249
+
250
+ ---
251
+
252
+ ## Card fields
253
+
254
+ Each card contains:
255
+
256
+ - Word (front)
257
+ - Class (part of speech, written in the same language as the Definition)
258
+ - Definition (in DEF_LANG or native language)
259
+ - 1st Example Sentence (from dictionary, in original language)
260
+ - 2nd Example Sentence (from dictionary, in original language)
261
+ - Example from Youtube Video (transcript sentence)
262
+ - Synonyms (in original language)
263
+ - Antonyms (in original language)
264
+ - VideoID and Source (where the card came from)
265
+ - IPA (pronunciation transcription, in the original language)
266
+ - Pronunciation (the recording itself, embedded so it plays in the card)
267
+
268
+ Everything describing the word stays in the transcript language, including
269
+ the recording. A German word defined in French is still pronounced in
270
+ German.
271
+
272
+ Fields are appended, never reordered. Indices 0-11 are what every
273
+ already-imported card is bound to. Adding one is a notetype schema change:
274
+ Tango aligns your collection's notetype before importing, and Anki will ask
275
+ for one full sync afterwards. See `CHANGELOG.md` for the v0.5.0 migration.
276
+
277
+ ---
278
+
279
+ ## Project structure
280
+
281
+ ```
282
+ tango/
283
+ ├── src/pipeline/
284
+ │ ├── config.py config and environment variables
285
+ │ ├── language.py language resolution and BCP-47 mapping
286
+ │ ├── translation.py argostranslate integration and mirror fallback
287
+ │ ├── transcript.py YouTube transcript extraction
288
+ │ ├── nlp.py spaCy vocabulary extraction
289
+ │ ├── deck.py AnkiConnect duplicate detection
290
+ │ ├── definition.py definition fetching and caching
291
+ │ ├── cards.py Anki card and package generation
292
+ │ ├── state.py SQLite state management
293
+ │ └── __main__.py CLI entry point
294
+ ├── tests/
295
+ ├── docs/
296
+ ├── pyproject.toml
297
+ └── Makefile
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Roadmap
303
+
304
+ Goals are tracked per release tag in **[ROADMAP.md](docs/planning/ROADMAP.md)**, which also
305
+ records what v1.0.0 freezes and what is deliberately out of scope.
306
+
307
+ **v1.0.0 is a finished CLI** — installable from a package, running on
308
+ Windows, macOS and Linux, on low-end and high-end hardware alike.
309
+
310
+ | tag | goal |
311
+ |---|---|
312
+ | v0.5.0 | pronunciation on cards, and a notetype that merges *(current)* |
313
+ | v0.5.1 | pronunciation for every language, starting with English |
314
+ | v0.6.0 | card quality |
315
+ | v0.7.0 | the command line as a product |
316
+ | v0.8.0 | runs on any operating system |
317
+ | v0.9.0 | runs on modest hardware |
318
+ | v0.10.0 | packaged and installable |
319
+ | v1.0.0 | a finished CLI |
320
+
321
+ A browser extension, a web or desktop app, and distribution to other
322
+ language ecosystems are **out of scope** for 1.0.0 — plausibly a separate
323
+ project sharing a common premise. See [ROADMAP.md](docs/planning/ROADMAP.md) §4.
324
+
325
+ Release history is in **[CHANGELOG.md](CHANGELOG.md)**.
326
+
327
+ ---
328
+
329
+ ## Requirements
330
+
331
+ - Python 3.10+
332
+ - Anki desktop with AnkiConnect add-on (code: 2055492159)
333
+ - Merriam-Webster API key (free tier)
334
+ - spaCy model: python -m spacy download en_core_web_sm
335
+
336
+ ---
337
+
338
+ ## License
339
+
340
+ MIT
@@ -0,0 +1,290 @@
1
+ # Tango
2
+
3
+ [![CI](https://github.com/AlphaNerdFx/Tango/actions/workflows/ci.yml/badge.svg)](https://github.com/AlphaNerdFx/Tango/actions/workflows/ci.yml)
4
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
6
+ [![Version](https://img.shields.io/badge/version-v0.5.3-orange)](https://github.com/AlphaNerdFx/Tango/releases/tag/v0.5.3)
7
+
8
+ Turn any YouTube video into Anki flashcards, automatically.
9
+
10
+ ---
11
+
12
+ ## What it does
13
+
14
+ You give Tango a YouTube video ID. It gives you an Anki .apkg file ready to import.
15
+
16
+ ```
17
+ YouTube video -> transcript -> spaCy NLP -> deck check -> definitions -> Anki cards
18
+ ```
19
+
20
+ Between extraction and card creation, Tango:
21
+
22
+ - Resolves the target language from a flag or deck name and fetches the right subtitles
23
+ - Prefers manually created transcripts over auto-generated ones
24
+ - Filters vocabulary by part of speech (nouns, verbs, adjectives, adverbs)
25
+ - Checks your existing Anki deck for duplicates using a three-condition fuzzy match that handles morphologically rich languages
26
+ - Detects sentence-structured decks and skips fuzzy matching where it would not be meaningful
27
+ - Fetches example sentences, synonyms, and antonyms in the original transcript language
28
+ - Fetches the definition in your chosen output language (English by default, or native)
29
+ - Builds cards with up to two dictionary examples, a video transcript example, synonyms, and antonyms
30
+ - Creates minimal fallback cards for words with no definition found
31
+
32
+ ---
33
+
34
+ ## Quick start
35
+
36
+ Prerequisites: Python 3.10+, [Anki](https://apps.ankiweb.net/) desktop, [AnkiConnect](https://ankiweb.net/shared/info/2055492159) add-on.
37
+
38
+ ```bash
39
+ pip install tango-anki
40
+ python -m spacy download en_core_web_sm
41
+ tango run <video-id> --deck "MyDeck"
42
+ ```
43
+
44
+ The command is `tango`; the package is `tango-anki`, because `tango` on PyPI
45
+ is an unrelated project. Run `tango doctor` at any point and it reports what
46
+ is installed, what is missing, and the command that fixes each.
47
+
48
+ Then import the generated .apkg from `output/` into Anki, or say yes when it
49
+ offers to import for you.
50
+
51
+ <details>
52
+ <summary>Working on Tango itself?</summary>
53
+
54
+ ```bash
55
+ git clone https://github.com/AlphaNerdFx/Tango.git
56
+ cd Tango
57
+ make all
58
+ cp .env.example .env
59
+ make run VIDEO_ID=<id> DECK="MyDeck"
60
+ ```
61
+
62
+ </details>
63
+
64
+ **Learning a language other than English?** Build its offline dictionary
65
+ first, or every card will read "No definition found":
66
+
67
+ ```bash
68
+ make dictionary LANGUAGE=fr # any code from `tango languages`
69
+ ```
70
+
71
+ One large download per language (a few hundred MB), then it works offline
72
+ forever. See [Definition coverage](#definition-coverage) for why this is
73
+ needed and what it gives you.
74
+
75
+ ---
76
+
77
+ ## Configuration
78
+
79
+ All configuration lives in `.env`. Copy `.env.example` (or run `make setup` for
80
+ a guided walkthrough) and fill in what you need. Nothing here is required to
81
+ run the pipeline:
82
+
83
+ | Variable | Required | Description |
84
+ |---|---|---|
85
+ | MW_API_KEY | No | [Merriam-Webster API key](https://dictionaryapi.com/register/index.htm) (free, 1000 requests/day). Improves English definitions; dictionaryapi.dev is used automatically without one |
86
+ | 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 |
87
+ | WEBSHARE_USERNAME, WEBSHARE_PASSWORD | No | Alternative to the above if you specifically use Webshare |
88
+ | 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 |
89
+ | LIBRETRANSLATE_URL | No | Local LibreTranslate server URL for translation mode |
90
+
91
+ ### Proxy notes
92
+
93
+ Most users never need a proxy. Requests come from your own residential IP by
94
+ default, which is exactly the traffic YouTube doesn't aggressively block.
95
+
96
+ Webshare's free tier was tested and made things worse, not better: transcript
97
+ extraction failed with repeated 429 errors through the proxy but succeeded
98
+ without it, because free-tier datacenter IPs get blocked more aggressively
99
+ than residential ones. This project doesn't recommend a specific provider,
100
+ free or paid. If you're actually getting rate-limited, bring your own
101
+ reputable proxy (a paid residential/mobile proxy you already trust, or a
102
+ personal VPN).
103
+
104
+ `LANGUAGE` and `DEF_LANG` are not `.env` variables. They're `make run` command
105
+ arguments (`make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr DEF_LANG=en`), see
106
+ below. Setting them in `.env` has no effect.
107
+
108
+ ### WSL
109
+
110
+ One setting, and it is in Anki rather than here: AnkiConnect binds to
111
+ 127.0.0.1, which WSL cannot reach. Change it to `0.0.0.0` in Anki under
112
+ Tools, Add-ons, AnkiConnect, Config.
113
+
114
+ You no longer need to set `ANKI_HOST`. Anki runs on the Windows side and
115
+ `localhost` from inside WSL is the Linux VM, so the connection is refused;
116
+ Tango notices, retries once against the Windows host it finds in the routing
117
+ table, and stays on whichever address answered. The old advice was to paste
118
+ that address into `.env` yourself, which worked until Windows rebooted and
119
+ reassigned it.
120
+
121
+ Setting `ANKI_HOST` explicitly still overrides all of this, and is never
122
+ second-guessed.
123
+
124
+ ---
125
+
126
+ ## Commands
127
+
128
+ ```bash
129
+ make run VIDEO_ID=<id> DECK="Deck::Name" run full pipeline
130
+ make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr specify subtitle language
131
+ make run VIDEO_ID=<id> DECK="French" LANGUAGE=fr DEF_LANG=en English definitions
132
+ make review DECK="Deck::Name" process deferred review.json
133
+ make backlog DECK="Deck::Name" process Anki backlog
134
+ make translate-setup install translation model
135
+ make test unit tests, no network needed
136
+ make test-all full suite with integration tests
137
+ make format auto-format with black
138
+ make lint lint with ruff
139
+ make clean remove venv, output, and cache
140
+ ```
141
+
142
+ To list all supported language codes:
143
+
144
+ ```bash
145
+ tango languages
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Language support
151
+
152
+ 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.
153
+
154
+ 40 languages are supported including French, Spanish, German, Japanese, Arabic, Russian, Chinese, Korean, and more.
155
+
156
+ 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.
157
+
158
+ Translation between languages uses argostranslate locally or community LibreTranslate mirrors. Run make translate-setup to install the local model for your language pair.
159
+
160
+ ### Definition coverage
161
+
162
+ English is covered by Merriam-Webster and dictionaryapi.dev out of the box, at around 98%.
163
+
164
+ **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.
165
+
166
+ The offline dictionary is built from Wiktionary data and works with no network access once built. Measured against real generated decks:
167
+
168
+ | language | definitions | examples | synonyms | antonyms |
169
+ |---|---|---|---|---|
170
+ | French | 95% | 92% | 83% | 20% |
171
+ | German | 91% | 84% | 60% | 51% |
172
+ | Russian | 91% | 72% | 72% | 46% |
173
+
174
+ 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.
175
+
176
+ 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`.
177
+
178
+ 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:
179
+
180
+ ```bash
181
+ make antonyms
182
+ ```
183
+
184
+ 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.
185
+
186
+ ---
187
+
188
+ ## How duplicate detection works
189
+
190
+ Tango compares each extracted lemma against your existing deck's card fronts using three conditions that must all pass.
191
+
192
+ WRatio above 90: word already in deck, skipped.
193
+ WRatio between 60 and 90, token sort ratio above 50, and length ratio above 0.6: possible duplicate, you decide at the prompt.
194
+ Anything else: new word, definition fetched and card created.
195
+
196
+ The three-condition filter prevents false positives in morphologically rich languages. "commencer" no longer incorrectly matches "comme" even though WRatio scores it at 90.
197
+
198
+ Sentence-structured decks skip fuzzy matching entirely and use exact match only.
199
+
200
+ ---
201
+
202
+ ## Card fields
203
+
204
+ Each card contains:
205
+
206
+ - Word (front)
207
+ - Class (part of speech, written in the same language as the Definition)
208
+ - Definition (in DEF_LANG or native language)
209
+ - 1st Example Sentence (from dictionary, in original language)
210
+ - 2nd Example Sentence (from dictionary, in original language)
211
+ - Example from Youtube Video (transcript sentence)
212
+ - Synonyms (in original language)
213
+ - Antonyms (in original language)
214
+ - VideoID and Source (where the card came from)
215
+ - IPA (pronunciation transcription, in the original language)
216
+ - Pronunciation (the recording itself, embedded so it plays in the card)
217
+
218
+ Everything describing the word stays in the transcript language, including
219
+ the recording. A German word defined in French is still pronounced in
220
+ German.
221
+
222
+ Fields are appended, never reordered. Indices 0-11 are what every
223
+ already-imported card is bound to. Adding one is a notetype schema change:
224
+ Tango aligns your collection's notetype before importing, and Anki will ask
225
+ for one full sync afterwards. See `CHANGELOG.md` for the v0.5.0 migration.
226
+
227
+ ---
228
+
229
+ ## Project structure
230
+
231
+ ```
232
+ tango/
233
+ ├── src/pipeline/
234
+ │ ├── config.py config and environment variables
235
+ │ ├── language.py language resolution and BCP-47 mapping
236
+ │ ├── translation.py argostranslate integration and mirror fallback
237
+ │ ├── transcript.py YouTube transcript extraction
238
+ │ ├── nlp.py spaCy vocabulary extraction
239
+ │ ├── deck.py AnkiConnect duplicate detection
240
+ │ ├── definition.py definition fetching and caching
241
+ │ ├── cards.py Anki card and package generation
242
+ │ ├── state.py SQLite state management
243
+ │ └── __main__.py CLI entry point
244
+ ├── tests/
245
+ ├── docs/
246
+ ├── pyproject.toml
247
+ └── Makefile
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Roadmap
253
+
254
+ Goals are tracked per release tag in **[ROADMAP.md](docs/planning/ROADMAP.md)**, which also
255
+ records what v1.0.0 freezes and what is deliberately out of scope.
256
+
257
+ **v1.0.0 is a finished CLI** — installable from a package, running on
258
+ Windows, macOS and Linux, on low-end and high-end hardware alike.
259
+
260
+ | tag | goal |
261
+ |---|---|
262
+ | v0.5.0 | pronunciation on cards, and a notetype that merges *(current)* |
263
+ | v0.5.1 | pronunciation for every language, starting with English |
264
+ | v0.6.0 | card quality |
265
+ | v0.7.0 | the command line as a product |
266
+ | v0.8.0 | runs on any operating system |
267
+ | v0.9.0 | runs on modest hardware |
268
+ | v0.10.0 | packaged and installable |
269
+ | v1.0.0 | a finished CLI |
270
+
271
+ A browser extension, a web or desktop app, and distribution to other
272
+ language ecosystems are **out of scope** for 1.0.0 — plausibly a separate
273
+ project sharing a common premise. See [ROADMAP.md](docs/planning/ROADMAP.md) §4.
274
+
275
+ Release history is in **[CHANGELOG.md](CHANGELOG.md)**.
276
+
277
+ ---
278
+
279
+ ## Requirements
280
+
281
+ - Python 3.10+
282
+ - Anki desktop with AnkiConnect add-on (code: 2055492159)
283
+ - Merriam-Webster API key (free tier)
284
+ - spaCy model: python -m spacy download en_core_web_sm
285
+
286
+ ---
287
+
288
+ ## License
289
+
290
+ MIT