6.5 KiB
CLAUDE.md — Guardian Daily Newscast Pipeline Guide
Project Overview
A Python pipeline that turns The Guardian's daily US news into MP3 newscast audio files.
Three scripts run in sequence:
- main.py — Scrapes Guardian articles (outputs
data/output.json) - reader.py — Generates teleprompter scripts via local LLM (outputs
data/scripts.json) - tts_generator.py — Converts scripts to MP3 audio (outputs
data/audio/*.mp3)
Coding Standards
- Language: Python 3.x
- Style: PEP 8 compliant.
- Async: Use
asyncioandhttpx.AsyncClient. Always ensure clients are closed properly (e.g., viaawait client.close()). - Logging: Use
logurufor all logging. - Typing: Use Python type hints throughout.
- Data Validation: Use Pydantic models for any structured data.
Architecture & Key Patterns
Pipeline Overview
[Stage 1] main.py (Guardian Scraper) --> data/output.json
[Stage 2] reader.py (LLM Script Gen) --> data/scripts.json
[Stage 3] tts_generator.py (TTS) --> data/audio/*.mp3
Each stage reads the previous stage's output file. Stages are independent and can be re-run in any order (but logically must follow 1 → 2 → 3).
Stage 1: Guardian Scraper (main.py + scraper/)
Client-Parser-Model Pattern:
GuardianClient(scraper/client.py): Handles HTTP networking, rate limits, and concurrency viaasyncio.Semaphore. Implements exponential backoff for 429/5xx responses.GuardianParser(scraper/parser.py): Pure functions for HTML extraction. Body fallback logic: (1)div[data-gu-context="body"], (2)<article>, (3)<main>.Article/ScrapeResult(scraper/models.py): Pydantic models for data consistency.utils.py: Disk I/O (save_results_to_json).
Stage 2: LLM Script Generator (reader.py)
- Sends each article body to Ollama (
http://127.0.0.1:11434/api/generate, modelgemma4:e2b). - System prompt enforces clean teleprompter-ready text (no stage directions, no speaker labels).
- Retry logic: up to 3 attempts with exponential backoff (10s, 20s). 2s delay between requests.
- Incremental save to
data/scripts.jsonduring processing.
Stage 3: TTS Audio Generator (tts_generator.py)
KokoroTTS: Wraps KokoroKPipelinewith voiceaf_heart, outputs WAV at 24kHz.convert_wav_to_mp3: ffmpeg subprocess (-q:a 2).ScriptItem: Pydantic model validating input fromdata/scripts.json.- Concurrent processing with
asyncio.Semaphore(CONCURRENCY_LIMIT=1). - Cleans up intermediate WAV files after conversion.
DevContainer (.devcontainer/)
A VS Code Dev Container enables reproducible development. Two files define it:
-
Dockerfile(based onpython:3.11-slim):- Installs system deps:
git,curl,ffmpeg. - Copies and installs
requirements.txt(project root) into the container at/app/. - Sets default env vars:
OLLAMA_URL,MODEL_NAME,PORT,POLL_INTERVAL,DATA_DIR,TTS_VOICE. - Creates
/app/data/audio/for output.
- Installs system deps:
-
devcontainer.json:- Points VS Code to the Dockerfile.
- Forwards port
8080(newscast API). postCreateCommand: runspip install -r requirements.txtin the VS Code Python interpreter for IDE features (linting, completions).remoteEnv: overrides Dockerfile defaults with workspace-relative values (e.g.,DATA_DIRpoints to${workspaceFolder}/data).- Extensions:
ms-python.python,ms-python.vscode-pylance.
Key env vars (Dockerfile defaults, overridable via devcontainer.json remoteEnv):
| Variable | Default | Description |
|---|---|---|
OLLAMA_URL |
http://host.docker.internal:11434/api/generate |
Ollama API endpoint (use host.docker.internal to reach host from container) |
MODEL_NAME |
gemma4:e2b |
Ollama model identifier |
PORT |
8080 |
Newscast API listen port |
POLL_INTERVAL |
21600 |
API polling interval in seconds (6 hours) |
DATA_DIR |
/app/data |
Base directory for all pipeline output files |
TTS_VOICE |
af_heart |
Kokoro TTS voice |
Common Tasks
Add a New Pipeline Stage
- Create a new script (e.g.,
stage_name.py) in the project root. - Read the previous stage's output file.
- Write results to a new output file.
- Add it to the
&&chain in README.md Quick Start. - Update the pipeline diagram in this document.
- Add the output file to the Output Files table.
Change Target URL (Stage 1)
Update the url variable in main.py (line 16). The URL is built dynamically from today's date:
url = f"https://www.theguardian.com/us-news/{year}/{month}/{day}/all"
Modify Concurrency (Stage 1)
Change the concurrency_limit passed to GuardianClient in main.py (line 19):
client = GuardianClient(concurrency_limit=5)
Adjust TTS Voice or Quality
- Voice: Change
VOICE = "af_heart"intts_generator.py(line 16). - Audio quality: Change
-q:a 2in theconvert_wav_to_mp3function (line 62). - Sample rate: Change
24000insf.write()(line 51).
Change LLM Model (Stage 2)
Update MODEL_NAME = "gemma4:e2b" in reader.py (line 8). Ensure the model is pulled in Ollama (ollama pull <model>).
Add Extraction Fields (Stage 1)
This is a three-step process:
- Update the
Articlemodel inscraper/models.py. - Update
extract_article_body(or add a new method) inscraper/parser.py. - Update the
Articleinstantiation inmain.py'sscrape_articlefunction.
Adjust Rate Limiting (Stage 1)
Modify the wait_time calculation in scraper/client.py (lines 36, 42).
Modify Retry Logic (Stage 2)
Change the retry count (line 40: if attempt < 3) and the backoff delay (line 41: wait = attempt * 10) in reader.py.
Update Data Flow Between Stages
When an output file format changes:
- Update the output format in the producing script (e.g.,
main.pystage 1). - Update the input format in the consuming script (e.g.,
reader.pystage 2). - Update type hints, Pydantic models, and JSON parsing in both files.
- Update this document's pipeline diagram and output tables.
Critical Files
main.py: Stage 1 orchestration (Guardian scraper).reader.py: Stage 2 orchestration (LLM script generation).tts_generator.py: Stage 3 orchestration (TTS audio generation).scraper/client.py: HTTP networking + concurrency control.scraper/parser.py: HTML extraction (Guardian page structure).scraper/models.py: Pydantic data schemas (Article, ScrapeResult, ScriptItem).scraper/utils.py: Disk I/O helpers.requirements.txt: Python dependencies.