Update README and add AGENTS.md
Assisted-by: Claude Opus 4.6 via pi
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..4d74e71
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,71 @@
+# Agents
+
+## Project overview
+
+Podcast transcript pipeline for Cooking Issues with Dave Arnold.
+Downloads episodes from two RSS feeds (Heritage Radio Network and
+Acast), transcribes audio through swappable backends, renders
+timestamped text, and generates a static site with full-text search
+via Pagefind.
+
+## Architecture
+
+```
+lib/config.rb — Config data class, all paths and URLs
+lib/feed.rb — Episode data class, RSS feed parser
+lib/episode_task.rb — Per-episode path derivation (slug, audio, JSON, text)
+lib/transcribers.rb — Transcriber classes (resolve, register, call, render)
+lib/download.rb — HTTP download helper with redirect following
+lib/pages/ — ERB templates for the static site
+bin/ — Self-contained uv Python scripts (mlx-transcribe, parakeet-transcribe)
+Rakefile — Pipeline orchestration, tasks
+```
+
+## Pipeline
+
+Each episode flows through: audio download → JSON transcription →
+text rendering. The JSON is an intermediate artifact stored in
+`cache/<transcriber>/`. The rendered text lives in
+`transcripts/<transcriber>/` and is committed to the repo. The
+static site is generated from the text files and is not committed.
+
+## Transcribers
+
+Each transcriber is a class in `lib/transcribers.rb` inheriting from
+`Transcribers::Base`. The interface:
+
+- `name` — directory name for output
+- `prereqs` — file paths the transcript task depends on
+- `register` — define Rake file tasks for setup (model downloads, builds)
+- `call(audio_path, transcript_path)` — run transcription, produce JSON
+- `render(json_path, txt_path)` — convert JSON to readable text
+
+Transcribers that don't need setup must still override `register` with
+a no-op — the base class raises `NotImplementedError`.
+
+The whisper-cpp transcribers take `cache_dir` in their constructor for
+model paths. Others use no constructor arguments.
+
+## Conventions
+
+- `Config.from_env` builds all configuration from env vars and defaults.
+ Don't scatter `ENV.fetch` calls or hardcoded paths through the codebase.
+- `EpisodeTask` wraps an episode and its index. Use it instead of
+ passing `(index, ep)` pairs.
+- Python scripts in `bin/` are self-contained uv scripts with inline
+ dependency metadata. They handle their own virtualenvs.
+- Transcription JSON formats vary by backend — each transcriber's
+ `render` method knows its own format.
+- Paragraph splitting heuristics differ per transcriber. Some use
+ silence gaps, some use sentence counts.
+
+## Tasks
+
+| Task | Description |
+|------|-------------|
+| `rake sync` | Download, transcribe, and render all episodes |
+| `rake episodes` | List episodes with transcription status |
+| `rake transcribe[N]` | Transcribe a single episode by number |
+| `rake retranscribe[N]` | Re-transcribe from scratch |
+| `rake pages` | Generate static site with Pagefind search |
+| `rake serve` | Build and serve the site locally on port 8000 |
diff --git a/README.md b/README.md
index 2e48ac0..6ae33bf 100644
--- a/README.md
+++ b/README.md
@@ -1,27 +1,28 @@
# Cooking Issues transcripts
-Transcripts of the [Cooking Issues](https://heritageradionetwork.org/series/cooking-issues/) podcast, hosted by Dave Arnold on Heritage Radio Network. Transcribed locally using swappable transcription backends.
+Transcripts of the [Cooking Issues](https://heritageradionetwork.org/series/cooking-issues/) podcast, hosted by Dave Arnold. Transcribed locally using swappable backends, searchable via a static site.
## How it works
-A Rake pipeline downloads episodes from the RSS feed, transcribes audio to JSON, and renders readable text. Each transcriber is a self-contained class that registers its own Rake tasks for setup and dependencies.
+A Rake pipeline pulls episodes from two RSS feeds (Heritage Radio Network and [Acast](https://shows.acast.com/cooking-issues-with-dave-arnold)), transcribes audio to JSON, renders readable text, and generates a searchable static site with [Pagefind](https://pagefind.app).
```
-RSS feed → download audio → transcribe (JSON) → render (text)
+RSS feeds → download audio → transcribe (JSON) → render (text) → static site
```
## Transcribers
-Set the backend with `TRANSCRIBER=name`. Each transcriber writes a JSON file with its native output format, then renders it to timestamped, paragraphed text.
+Set the backend with `TRANSCRIBER=name`.
| Name | Engine | Diarization | Notes |
|------|--------|-------------|-------|
-| `whisper-cpp-large` | whisper.cpp (large-v3-turbo) | No | Default. Fast on Apple Silicon via Metal. |
-| `whisper-cpp-tdrz` | whisper.cpp (small.en-tdrz) | tinydiarize | Lightweight model with basic speaker turns. |
+| `parakeet` | Parakeet TDT via parakeet-mlx | No | Fast on Apple Silicon. |
+| `whisper-cpp-large` | whisper.cpp (large-v3-turbo) | No | Fast on Apple Silicon via Metal. |
+| `whisper-cpp-tdrz` | whisper.cpp (small.en-tdrz) | tinydiarize | Lightweight with basic speaker turns. |
| `sous_chef` | Apple SpeechAnalyzer | No | macOS 26+ only. Word-level segments. |
-| `mlx` | mlx-whisper | No | Runs on Apple Silicon via MLX. |
-| `mlx-diarize` | mlx-whisper + pyannote | Yes | Speaker labels via pyannote. Requires HF token. |
-| `whisperx` | WhisperX | Yes | CPU-based, uses pyannote for diarization. |
+| `mlx` | mlx-whisper | No | Apple Silicon via MLX. |
+| `mlx-diarize` | mlx-whisper + pyannote | Yes | Speaker labels. Requires HF token. |
+| `whisperx` | WhisperX | Yes | CPU-based with pyannote diarization. |
## Usage
@@ -32,17 +33,23 @@ rake transcribe[42]
# Transcribe all episodes
rake sync
-# Re-transcribe (deletes existing JSON and text, then rebuilds)
+# Re-transcribe from scratch
rake retranscribe[42]
-# List episodes and their transcription status
+# List episodes with transcription status
rake episodes
# Use a different transcriber
-TRANSCRIBER=mlx rake transcribe[1]
+TRANSCRIBER=parakeet rake transcribe[1]
+
+# Generate the searchable static site
+rake pages
+
+# Build and serve locally
+rake serve
```
-Transcribers that use pyannote for diarization (`mlx-diarize`, `whisperx`) need a Hugging Face token with access to the [pyannote/speaker-diarization-3.1](https://hf.co/pyannote/speaker-diarization-3.1) and [pyannote/segmentation-3.0](https://hf.co/pyannote/segmentation-3.0) gated models:
+Transcribers that use pyannote (`mlx-diarize`, `whisperx`) need a Hugging Face token with access to the [pyannote/speaker-diarization-3.1](https://hf.co/pyannote/speaker-diarization-3.1) and [pyannote/segmentation-3.0](https://hf.co/pyannote/segmentation-3.0) gated models:
```sh
export HUGGING_FACE_TOKEN=hf_...
@@ -50,20 +57,24 @@ export HUGGING_FACE_TOKEN=hf_...
## Output
-Transcripts live in `transcripts/<transcriber>/`, with a JSON file (full transcription data) and a text file (rendered for reading) per episode:
+Intermediate JSON goes to `cache/<transcriber>/`. Rendered text is committed to `transcripts/<transcriber>/`, one file per episode:
```
-transcripts/whisper-cpp-large/001-episode-1-cooking-issues-debuts.json
-transcripts/whisper-cpp-large/001-episode-1-cooking-issues-debuts.txt
+transcripts/parakeet/001-episode-1-cooking-issues-debuts.txt
```
-The text renderer groups segments into paragraphs based on silence gaps between them. The gap threshold varies by transcriber since some produce sentence-level segments and others produce word-level segments.
+The text renderer groups segments into paragraphs. The heuristic varies by transcriber — silence gaps for most, sentence counts for Parakeet (whose chunking overlap produces overlapping timestamps).
+
+## Static site
+
+`rake pages` generates a static site in `pages/` with one HTML page per episode and a Pagefind search index. `rake serve` builds and serves it locally on port 8000. GitHub Actions deploys it to GitHub Pages on push to main.
## Requirements
- Ruby 3.x with Bundler
+- Python 3.10+ with [uv](https://github.com/astral-sh/uv) (for Parakeet, MLX, and Pagefind)
- At least one transcriber's dependencies:
- - **whisper-cpp**: `whisper-cli` (install via Homebrew or build from source)
+ - **parakeet**: uv (dependencies managed automatically)
+ - **whisper-cpp**: `whisper-cli` (Homebrew or build from source)
- **sous_chef**: macOS 26+, Xcode 26+ (builds automatically)
- - **mlx / mlx-diarize**: Python 3.10+, [uv](https://github.com/astral-sh/uv) (dependencies managed automatically via inline script metadata)
- - **whisperx**: Python 3.10+, uv
+ - **mlx / mlx-diarize / whisperx**: uv