33 Commits
Author SHA1 Message Date
lukeandClaude Opus 5 242eb15e0a Generate the episode page after publishing, not before
regenerate_website_pages() ran at step 3.7, before the step-4 publish call.
generate_episode_pages.py is driven entirely by the RSS feed, so the episode
being published was never in the data it read: the generator exited 0, printed
its success line, and silently omitted the new episode. Its page only appeared
on the NEXT publish.

Social posts link to /episode/<slug>/, so every launch-day link across X,
Bluesky, Discord, Instagram, Threads, Facebook, LinkedIn, Mastodon and Nostr
404'd during exactly the window the post was circulating. That held for all 58
published episodes.

The existing test only asserted the generator gets invoked with --sitemap,
which passed throughout. Added two tests pinning the call between the publish
and the wrangler deploy, verified to fail against the previous ordering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:05:17 -05:00
lukeandClaude Opus 5 b85e1c6ea7 Fix caller lineup never loading and add Wellspring cult callers
The lineup fix and the new characters both land in main.py, so they share a
commit rather than being split artificially.

Lineup fix — populate_backgrounds() was reachable only through
POST /api/session/reset, so taking calls without hitting reset left
session.caller_backgrounds empty for a whole show. Every caller got a hollow
prompt, collapsed into generic relationship filler, and Silas lost his lore.
Now start_call populates on demand, get_caller_prompt raises
EmptyCallerBackgroundError instead of emitting an empty prompt, /api/callers
reports lineup readiness to a header badge, and cross-episode topic dedup
widened from 2 shows to 10.

Wellspring callers — Doyle as the anchor defector plus a six-member pool,
grouped by new frontmatter fields (group, group_lead, group_weight, register,
explicitness). A faction is capped at one caller per show, with a 15% roll for
two pairing the lead with one other member. group_weight lets an anchor carrying
an arc get slots faster than texture characters.

Also detects model refusals, which arrive as ordinary 200s and previously
reached air as broken-character text or dead silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 03:33:08 -05:00
lukeandClaude Opus 5 2071ae4181 Track four test files that were never committed
CLAUDE.md cites tests/test_model_config.py as the guard against reintroducing
retired OpenRouter model ids, but the file existed only on disk — the guard was
not in the repository. Same for the town geo, voice roster and voicemail
transcript tests, and the Reaper bleep-selection test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 04:23:44 -05:00
lukeandClaude Opus 5 44ef13336e Move caller dialog to Sonnet 4.6, add SearXNG for Devon, and fix stale model ids
Working-tree changes that had accumulated without being committed. The test
updates matter most: tests/test_caller_gen.py was left behind when caller_gen
started requiring voice and age in regulars_included, so the committed tree had
a failing suite that only passed locally.

- Caller dialog moves from Haiku 4.5 to Sonnet 4.6 (~$1/show to ~$3-4/show)
- Grok pinned to x-ai/grok-4.3; grok-4, grok-4-fast and grok-4.1-fast were
  retired from OpenRouter, and llm.py swallows the 404 and returns empty text,
  so a retired id makes callers go silent with nothing in the logs
- Devon's web_search now runs against SearXNG on the NAS
- Assorted TTS, audio, news, cost tracker and control-panel changes
- CLAUDE.md updated to match

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 04:21:55 -05:00
lukeandClaude Opus 5 2901e5f4fb Stop Whisper misspelling the intern's name as Devin
The intern reached 21 of 58 published transcripts as "Devin". The obvious fix —
seed his name in the Whisper initial prompt — only works for one of the two
transcription paths:

- backend/services/transcription.py (live show) uses mlx_whisper and does take
  an initial_prompt. Added Devon there. This also picks up the Big Bend prompt
  rewrite from the relocation work, which was sitting uncommitted.
- publish_episode.py, which actually produces the published transcripts, uses
  LightningWhisperMLX, whose transcribe() signature is (audio_path, language).
  It accepts no initial_prompt at all, so there is nothing to seed.

So the published path gets a deterministic correction pass instead:
fix_proper_nouns() rewrites known mishearings after transcription, preserving
casing (Devin/DEVIN/devin -> Devon/DEVON/devon) and matching whole words only,
so "Devinshire" is left alone. Verified against the real episode 58 transcript:
24 occurrences to 0, output byte-identical to the manual relabel in bf1afef.

Swapping the publish path to mlx_whisper would allow a real prompt, but that
changes the transcription engine for every episode and is a bigger call than
this warrants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 04:20:22 -05:00
lukeandClaude Opus 5 bf1afef351 Correct the intern's name to Devon across all transcripts
Whisper transcribed the intern as "Devin" in 21 of 58 transcripts — he is Devon
everywhere else: backend/services/intern.py, the website, and the show's lore.
Five files used both spellings for the same character, so this was transcription
drift rather than two people. Context confirms every instance is the intern
("our new intern, Devin here", "Devin, where's my coffee?").

436 replacements across 22 files, 384 insertions against 384 deletions — pure
substitution, no content added or lost. Speaker labels went from 219 DEVIN: /
17 DEVON: to 236 DEVON:.

This mattered now because the transcripts stop being .txt files nobody reads and
become indexed text on 57 episode pages.

Root cause is unfixed: the Whisper initial prompt in transcription.py does not
seed "Devon" as a proper noun, so new episodes will drift again. Added a test
that fails if any transcript reintroduces the misspelling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 04:15:08 -05:00
lukeandClaude Opus 5 2429a2eb07 Regenerate episode pages and sitemap during publish
add_episode_to_sitemap() was removed in the previous commit, so nothing was
writing the sitemap any more. Publishing now shells out to
generate_episode_pages.py --sitemap right after the transcript is copied into
website/transcripts/, which builds the new episode's static page and rebuilds
the sitemap from the feed.

Deliberately non-fatal: by this point the audio is live on Castopod and the RSS
feed has been rebuilt, so a generator failure must not abort the publish. It
warns and moves on, and the page lands on the next run. Covered for non-zero
exit, timeout, and a missing generator script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 04:12:29 -05:00
luke fa5ab9aad2 Point internal links at clean episode URLs and retire the JS page 2026-08-14 04:10:03 -05:00
luke b496b91a5d Generate sitemap from feed with clean episode URLs 2026-08-14 04:05:45 -05:00
luke 6951398872 Add episode page generator CLI 2026-08-14 03:58:24 -05:00
lukeandClaude Opus 5 61d24ae7d8 Style transcript turns and bump style.css cache version
The renderer emits .transcript-turn and .transcript-speaker, neither of which
had any CSS, so speaker labels and turns rendered unstyled. Extends the
existing .speaker-label rule rather than duplicating it.

Bumps style.css to v=7 everywhere — the file changed, and pages referenced a
mix of v=5 and v=6, so some visitors would have kept a cached copy without the
new rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 03:52:06 -05:00
luke 56a6a2dfbe Add episode page renderer with PodcastEpisode schema 2026-08-14 03:46:01 -05:00
luke fdb9f57660 Add RSS feed loader for episode page generation 2026-08-14 03:41:53 -05:00
luke cc0d9ffc5d Add transcript parser for episode page generation 2026-08-14 03:34:14 -05:00
lukeandClaude Opus 5 00ddc2d1b4 Recover real episode metadata on --resume instead of rebuilding from the slug
A --resume run rebuilt the episode title from the Castopod URL slug, which is
lowercased and stripped of punctuation, so episode 58 went to YouTube as
"Episode 58: Rayfield S Nephew The Marfa Lights And Why Nobody Believes Concho"
with a placeholder description. save_chapters() only persisted chapters, so
resume had no real metadata to fall back on.

Metadata is now written to <audio>.metadata.json during a normal run and read
back on resume. If that file is missing (episodes published before this change)
the title and description are read from Castopod instead. Slug reconstruction
survives only as a last resort: it warns loudly, flags the result, and anchors
the episode prefix strip so it no longer eats the phrase mid-title.

Reading back from Castopod needed its own fix — TO_BASE64 wraps every 76 chars
and mysql renders those breaks as a literal backslash-n, which made b64decode
throw and the lookup silently return None.

Also includes two changes that were already sitting in the working tree: the
YouTube tag budget fix that episode 58's invalidTags failure prompted (with its
test), and a metadata model bump to claude-haiku-4.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 02:54:29 -05:00
luke 76feeb22a0 Collapse caller generation to single path, remove feature flag 2026-04-05 04:05:09 -06:00
luke 604769a949 Tighten slim prompt: hard-ban stage directions 2026-04-05 03:15:53 -06:00
luke a18e1329f3 Add slim caller prompt builder (~400 tokens) 2026-04-05 03:01:16 -06:00
luke 2fd027b82a Add writer for promoted tier-2 regulars 2026-04-05 02:56:20 -06:00
luke 0c5d36d182 Strip markdown fences in _call_sonnet response 2026-04-05 02:46:38 -06:00
luke d7e475331d Add promotion gate for tier-2 regulars 2026-04-05 02:43:32 -06:00
luke 470e92f8c4 Add Regular dataclass + lore file loader 2026-04-05 02:36:25 -06:00
luke b792c3cca0 Strip markdown fences in parse_batch_response 2026-04-05 02:31:17 -06:00
luke dec6211f7c Add batch prompt builder for caller_gen 2026-04-05 02:23:34 -06:00
luke ade5510bd5 Add voice roster validator for caller_gen 2026-04-05 02:22:10 -06:00
luke f56ca3d4bc Add CallerIdentity dataclass and batch JSON parser 2026-04-05 02:14:58 -06:00
lukeandClaude Opus 4.6 437980dfd4 Update tests for SignalWire phone caller format
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 17:47:50 -07:00
lukeandClaude Opus 4.6 a72c1eb795 Update tests for CallerService and browser caller format
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 15:53:41 -07:00
lukeandClaude Opus 4.6 3961cfc9d4 Rename TwilioService to CallerService, remove Twilio-specific audio encoding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 15:45:08 -07:00
lukeandClaude Opus 4.6 141f81232e Add AI follow-up system with call summarization and show history
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 13:42:35 -07:00
lukeandClaude Opus 4.6 c82420ddad Add outbound audio streaming to real callers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 13:39:02 -07:00
lukeandClaude Opus 4.6 924ddca71a Add Twilio call queue service with channel allocation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 13:31:02 -07:00
lukeandClaude Opus 4.6 00c2e8f018 Add Session multi-party call support and show history
- Add CallRecord dataclass for tracking call history
- Extend Session with call_history, active_real_caller, ai_respond_mode, auto_followup
- Add get_show_history() for AI caller context about earlier calls
- Update get_conversation_summary() to handle host/real_caller/ai_caller roles
- Update reset() to clear new fields
- Add tests for all new functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 13:25:32 -07:00