- Shell 94.1%
- Nix 5.9%
|
|
||
|---|---|---|
| bin | ||
| languages | ||
| .gitignore | ||
| CONTEXT.md | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
language-tutor
CLI tooling for self-studying spoken languages from real videos. It pulls captions and comments, generates practice audio (single-voice drills or two-voice conversational podcasts), and publishes it to a private Audiobookshelf library on the homelab NAS.
Built for and run on the laptop. No system packages — everything comes from the flake dev shell. Learning content (word-by-word breakdowns, notes, zettels) lives in the Obsidian zettelkasten, not in this repo.
Two TTS engines
- Piper (
say) — fully local, offline. Used for single-voice practice scripts with one navigable chapter per sentence. - edge-tts (
podcast) — Microsoft neural voices, online. Used for two-voice conversational podcasts, because Piper has no female Brazilian voice (all four pt_BR Piper voices are male). edge-tts has real pt-BR male (Antonio) and female (Francisca) voices. The script text is sent to Microsoft at render time; these are just kids'-video vocab scripts, so nothing sensitive leaves the network.
Quick start
nix develop # piper, edge-tts, yt-dlp, jq, ffmpeg, rsync on PATH + bin/
get-voice pt-BR # one-time: download the Piper voice (for `say`)
captions --comments <url> # clean transcript + top comments for a breakdown
new-video <url> # stub a breakdown note in the zettelkasten
say -f script.txt # single-voice chaptered MP3 -> NAS -> Audiobookshelf
podcast -o "01 - Title" -f episode.md # two-voice podcast -> NAS -> Audiobookshelf
Commands (bin/)
| Command | What it does |
|---|---|
get-voice [lang] |
Download the language's Piper voice from Hugging Face into ~/.local/share/piper/voices/. |
captions [--lang L] [--comments] <url> |
Print a video's auto-captions as clean text; optionally the top comments. |
new-video [--lang L] <url> |
Fetch a transcript and drop a breakdown stub into the vault. |
say [--lang L] [-o name] (text or -f file) |
Single voice (Piper): synthesize a chaptered MP3 (one chapter per line) and rsync it to the NAS. |
podcast [--lang L] -o name -f script.md |
Two voices (edge-tts): render a speaker-tagged conversation to a chaptered MP3 and rsync it to the NAS. |
Two-voice podcasts (podcast)
podcast reads a note's ## Script section, where each bullet is either a chapter marker or
a spoken turn:
## Script
- # Abertura <- starts a navigable chapter
- Ana: Oi, gente! Tudo bom? <- one turn, voiced by Ana's voice
- Bruno: E aí, pessoal! <- one turn, voiced by Bruno's voice
Speaker labels map to edge-tts voices via PODCAST_CAST in languages/<lang>.env
(e.g. Ana=pt-BR-FranciscaNeural;Bruno=pt-BR-AntonioNeural). A silence of TURN_GAP
seconds is inserted between turns for natural breathing room. Each episode is one MP3 file;
several episodes sit side by side under one Audiobookshelf book (<author>/<book>/NN - x.mp3),
so the show reads as one book with the episodes as its tracks.
For multi-character stories, a note can declare its own cast in a ## Cast section, which
overrides the env defaults so each character gets a distinct voice:
## Cast
- Narrador: pt-BR-AntonioNeural
- Bia: pt-BR-FranciscaNeural
- Vó Lúcia: pt-BR-ThalitaNeural
Use --book "Season 2 - Histórias" to drop an episode into a different book/season than the
env default.
pt-BR voice caveat: on the free edge-tts endpoint only three Brazilian voices actually return audio —
pt-BR-AntonioNeural(M),pt-BR-FranciscaNeural(F), andpt-BR-ThalitaNeural(F). The other listed pt-BR voices fail withNoAudioReceived(they need a paid Azure key). So a scene can have at most one distinct male voice; cast stories accordingly (narrator + up to two characters, at most one male per scene).
How the audio reaches the phone
Both tools rsync to an Audiobookshelf Books library on the NAS, and the phone streams via the ABS app.
saywritesAUDIO_OUT/<name>/<name>.mp3→NAS_DEST/<name>/, so each practice script becomes its own audiobook with one chapter per sentence.podcastwritesPODCAST_AUDIO_OUT/<book>/<name>.mp3→PODCAST_AUTHOR_DIR/<book>/<name>.mp3. The author folder is the show (Portuguese Conversations), the book is a season (Season 1), and each episode is a separate file (track) under that one book, with topic chapters inside.
Use a Books library, not Podcasts — ABS podcast libraries do not read embedded chapters from local files, so the chapters would not show.
Languages
One languages/<code>.env per language sets the voice, caption code, paths, and pacing
(LENGTH_SCALE, SENTENCE_GAP). Add a language by dropping in a new env and running
get-voice <code>. Outputs stay separated by language.
Layout
flake.nix dev shell (piper-tts, edge-tts, yt-dlp, jq, ffmpeg, rsync)
languages/<code>.env per-language config (Piper voice + podcast cast/pacing/paths)
bin/ get-voice, captions, new-video, say, podcast
Voice models, generated audio, and caption files are never committed (see .gitignore).