Fully-local, Tagesschau-style English news bulletin generator. Pulls fresh RSS, has a local LLM curate and write the bulletin, grounds it in full article text, fact-checks it, then a local MLX text-to-speech model voices it — no cloud LLM or TTS calls. Built for Apple Silicon.
ingest -> RSS feeds fetched in parallel (async httpx)
curate -> semantic dedup (local embeddings) + local LLM ranks/picks 6 stories
hydrate -> full article text fetched + extracted (trafilatura) for the picks
write -> local LLM writes greeting + headline teasers + one turn per story
verify -> local LLM fact-checks every claim against sources, cuts unsupported
assemble -> markets brief + Munich weather built deterministically from real data
synth -> VibeVoice (MLX): anchor voice + weather voice; ffmpeg loudnorm -16 LUFS
publish -> (optional) Google Drive upload + email link
Structure: a single authoritative anchor reads the news; a second voice reads the weather. Market figures and weather come straight from live data (never phrased by the LLM), so those numbers are always exact. The writer LLM is unloaded before TTS so everything fits in 24 GB.
| Stage | Tech |
|---|---|
| Script | qwen3.5-9b via LM Studio (local, OpenAI-compatible server) |
| Dedup | nomic-embed embeddings via LM Studio (drop same-event stories) |
| Full text | trafilatura (real article body, not thin RSS summaries) |
| Voice | VibeVoice-Realtime-0.5B (MLX) — per-voice conditioning caches |
| Data | Open-Meteo (Munich weather), Yahoo Finance (DAX / S&P / EUR-USD) — keyless |
| Glue | async httpx, feedparser, pydantic, ffmpeg |
Anchor + weather voices map to bundled VibeVoice caches; swap voices/models/city
in podcast/config.py or via .env.
- Apple Silicon Mac, Python 3.11+
- LM Studio running its local server (port 1234) with a
writer model downloaded (
qwen3.5-9b-mlx), and an API token ffmpeg(for loudness normalization) — optional but recommended
uv venv && source .venv/bin/activate
uv pip install -e . # or: uv pip install -r <deps in pyproject>
cp .env.example .env # set LMSTUDIO_API_TOKENpython run.py # full pipeline -> output/episode.wavIndividual stages are runnable too:
python -m podcast.ingest # fetch RSS
python -m podcast.curate # dedup + pick stories
python -m podcast.extras # weather + markets probe
python -m podcast.write # curate -> write -> fact-check -> assemble
python -m podcast.synth # re-voice the current script.jsonoutput/content.json— fetched articlesoutput/curation.json— chosen storiesoutput/script.json— the bulletin script (anchor + weather turns)output/episode.wav— the finished episode
Set PUBLISH=1 plus Drive/SMTP vars in .env to upload the episode to Google
Drive and email the link.
| Var | Default | Notes |
|---|---|---|
WRITER_MODEL |
qwen3.5-9b-mlx |
any LM Studio model; qwen3.5-4b-mlx is lighter |
TTS_MODEL |
mlx-community/VibeVoice-Realtime-0.5B-fp16 |
|
TTS_DDPM_STEPS |
20 |
higher = better quality, slower |
ANCHOR_VOICE / WEATHER_VOICE |
en-Frank_man / en-Emma_woman |
bundled VibeVoice caches |
SPEED_ANCHOR / SPEED_WEATHER |
0.92 / 1.10 |
tempo, pitch-preserving; >1 = faster |
WEATHER_CITY / WEATHER_LAT / WEATHER_LON |
Munich / 48.137 / 11.575 |
weather segment |
gemma-4-26bwon't load on 24 GB (LM Studio guardrail) — too tight.gpt-oss-20bdegenerates under strict JSON output; avoid for this task.qwen3.5-9bis a reasoning model — it emits structured output in the reasoning channel, whichpodcast/llm.pyreads transparently.
MIT — see LICENSE. Note the local models carry their own licenses (VibeVoice: MIT; Qwen3.5: Qwen license) and are not redistributed here.