← runs · all docs · view source on github

src/

Deep system diagram. The top-level README.md has the user-facing intro; this is the architecture dive — read it when you need to know "if I touch X, what else moves?"

The two binaries

pipeline                generate
   │                       │
   ├── ingest              ├── concept-only mode → autorater → critique → loop
   ├── extract  (×10)      ├── full-sketch mode  → produce_audio → audio_eval → critique
   ├── merge               ├── meta mode         → run_meta (concept→episode→audio→eval, N iters)
   ├── analytics           └── audio mode        → produce_audio on an existing episode
   └── calibrate

Both pipeline and generate go through client.DoubleActsClient for every model call. That's the only module that talks to google-genai. Everything else is built on top.

Layered dependency map

                                 schemas.py        (Pydantic v2 — single source of truth for every model)
                                     │
                                     ▼
                                 paths.py          (every filesystem location; honors $DOUBLE_ACTS_DATA_ROOT)
                                     │
                                     ▼
                                 client.py         (only place google-genai is touched; retry+budget+call-log)
                                ╱  │  │  ╲
                               ╱   │  │   ╲
                              ▼    │  │    ▼
                       pricing.py  │  │  manifest.py    (corpus mp3 → sha256/uri/duration)
                                   │  │
                ┌──────────────────┼──┼──────────────────┐
                │                  │  │                  │
                ▼                  ▼  ▼                  ▼
         INGEST/EXTRACT       AUTORATER STACK       AUDIO STACK
         (pipeline only)      (text evaluation)     (generate only)

Layer A — Ingest & extract (the corpus side)

ingest.py     ──▶  download mp3 + sha256 + duration
                   ▼
manifest.py   ──▶  manifest.json (the corpus index — sha, uri, duration per ep)
                   ▼
extract.py    ──▶  10 extractors run in parallel via google-genai Files API
                   each writes RAW_EXTRACTIONS_DIR/<ep>/<extractor>.json
                   ▼
merge.py      ──▶  reconcile 10 JSONs into one canonical Episode → EPISODES_DIR/<ep>.json
                   ▼
analytics.py  ──▶  cross-corpus stats + patterns report
calibrate.py  ──▶  corpus vs. contrast separation; writes calibration_report.md
                   ▼
pipeline.py   ──▶  the runner — `pipeline --skip_ingest --skip_extract` etc.

Layer B — Generate (the output side)

generate.py        ──▶  concept generation, full sketch, scene-tree search, scene-edit-mode
                        functions: generate_concept, generate_full, generate_full_by_scene_tree,
                                   regenerate_scene, concept_to_episode
                        modes: iterate, meta, full, audio, iterate_then_audio
                        flags: --target_duration_min, --spec_decode, --draft_model,
                               --streaming_tts, --tree_branches, --early_exit_threshold
                        uses: corpus_exemplar (semantic exemplar lookup)
                   ▼
iterate.py         ──▶  the iter loop — concept hill-climb (`run`) + full pipeline (`run_meta`)
                        writes: <run>/<NN>/concept.json, episode.json, record.json, run_manifest.json
                        knows about scene-edit-mode, crossbreed, listener panel feedback
                        prints recommended next invocation (self-pacing)
                        helper: _synthesize_full_audio (dialogue + announcer + music + foley + mix)

Layer C — Text evaluation (the autorater stack)

autorater.py             ──▶  28 lint rules + 11-axis judge — composite text score
                              entry: score(client, episode, skip_judge=False)
personas.py                   the panel personas (writers, producers, listener types)
critique.py                   basic + segment + diff critique + durable lessons.md addendum
adversarial_critic.py         red/green/refactor — attacker → defender → patch
cross_model_judge.py          Claude Sonnet 4.6 same-axis ensemble (the second opinion)
calibrate.py                  drift detector — corpus−contrast separation tripwire
benchmark.py                  N=20 fixed-seed benchmark + corpus audio benchmark
report_card.py                aggregates EVERYTHING for one iteration into a markdown report

Layer D — Audio (the production stack)

produce_audio.py    ──▶  Gemini 3.1 Flash TTS multi-speaker (top-2 voices/chunk, ≤20 lines/chunk,
                         24 kHz mono PCM concat with 10 ms crossfades + 150 ms internal-silence
                         compression, gender + timbre voice routing, sticky per-chunk assignments,
                         per-speaker style cue + conversation framing, optional streaming path,
                         optional voice fingerprint chain across chunks)
                         entry: synthesize_episode(client, episode, out_dir, ...)
                                synthesize_announcer(...)
                                rerender_chunks(...) — surgical re-render around audio anti-pattern timestamps
produce_music.py    ──▶  Lyria realtime stream → 48 kHz stereo → downsample to 24 kHz mono
elevenlabs.py       ──▶  per-line ElevenLabs SFX (replaces the old foley library)
mix.py              ──▶  dialogue + announcer + music + sfx → final_mixed.wav
                         per-track gains, leading silence for opening music intros,
                         music sidechain ducking during detected speech (-10 dB, 80 ms ramps)

Layer E — Audio evaluation (audio_eval/)

audio_eval/__init__.py        umbrella: AudioEvaluationReport with per-axis sub-reports
   ├── audio_judge.py         8-axis multimodal judge — uploads wav, asks Gemini to score
   │                          also: per-chunk evaluator, persona panel, pairwise comparator
   ├── distinctness.py        deterministic — measures pitch/timbre separation between speakers
   ├── pacing.py              deterministic — words per minute vs. corpus target (135)
   ├── seam_check.py          deterministic — chunk-boundary discontinuity detection
   ├── stt_check.py           deterministic — Whisper-style STT roundtrip + WER vs. transcript
   └── synthesize_critique.py LLM — turns audio anti-patterns into next-iter critique notes

Layer F — Standalone runners ([project.scripts])

Command Module Purpose
pipeline pipeline.py ingest+extract+merge+analytics+calibrate
generate generate.py concept/full/iterate/meta/audio modes
compare-audio compare_audio.py A/B comparison runner over multiple wav variants
corpus-research corpus_research.py Gemini Deep Research Max over the 12 corpus MP3s
report-card report_card.py Hundreds-of-categories evaluation aggregator
cross-model-judge cross_model_judge.py Gemini + Claude ensemble per-axis comparison
listener listen_log.py Pairwise candidate dump for ear-vs-ear listening

Each runner uses absl.flags, calls client.set_run_id() once at startup so all calls in data/calls.jsonl share a run_id, and respects --budget_usd.

Cross-cutting conventions

Where to start reading