No description
  • Shell 94.1%
  • Nix 5.9%
Find a file
aaronjan 7f97f67c22 podcast: support per-speaker pitch (voice@±NHz) for same-gender voices
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-05 18:58:28 -03:00
bin podcast: support per-speaker pitch (voice@±NHz) for same-gender voices 2026-08-05 18:58:28 -03:00
languages add podcast: two-voice conversational TTS via edge-tts 2026-07-10 07:45:08 -07:00
.gitignore initial project scaffold 2026-06-27 15:49:07 -07:00
CONTEXT.md podcast: add --book flag and per-note ## Cast for multi-voice stories 2026-07-22 23:13:09 -03:00
flake.lock add chapters, sentence gaps, NAS publish, and README 2026-06-27 21:40:09 -07:00
flake.nix add podcast: two-voice conversational TTS via edge-tts 2026-07-10 07:45:08 -07:00
README.md podcast: add --book flag and per-note ## Cast for multi-voice stories 2026-07-22 23:13:09 -03:00

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), and pt-BR-ThalitaNeural (F). The other listed pt-BR voices fail with NoAudioReceived (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.

  • say writes AUDIO_OUT/<name>/<name>.mp3 → NAS_DEST/<name>/, so each practice script becomes its own audiobook with one chapter per sentence.
  • podcast writes PODCAST_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).