The worker rewrote <title> and og: tags for episode pages, but only when the
User-Agent matched a hardcoded social-crawler list. Googlebot, Bingbot, GPTBot,
ClaudeBot and PerplexityBot were all absent, so search and answer engines only
ever saw the generic shell. Serving crawlers different HTML than users is also
cloaking, which Google disallows.
Episode pages are static now, so the injection is dead code. Replaces it with a
301 from /episode.html?slug=X to /episode/X/. Published YouTube descriptions and
social posts still use the old form, so the redirect stays indefinitely.
The slug is restricted to [a-z0-9-] before it reaches the Location header:
verified that //evil.com, ../../etc/passwd, a CRLF payload and "><script> all
reduce to harmless on-domain paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Transcripts for episodes 39-58 existed only on disk. Wrangler deploys the
directory rather than the git tree, so the live site served them while git
never had them — a fresh clone or worktree came up 19 files short.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The site carried no geographic signal at all: the homepage title, description
and schema keywords never mentioned Alpine, Marfa or the Big Bend, and llms.txt
still described the show as broadcasting from a fictional desert hermit's RV
and being produced in New Mexico.
- Homepage title, description and OG/Twitter tags now lead with Alpine, Texas
and name the surrounding towns; keywords put the location terms first
- PodcastSeries schema gains contentLocation with Alpine's real coordinates
(from BIG_BEND_TOWNS), spatialCoverage for the region, and an about array
covering Alpine, Marfa, Terlingua, Big Bend National Park and the Marfa Lights
- llms.txt: corrects the setting throughout, adds the region to Key Facts, and
answers "what places does the show talk about"
- Fixes the stale "37+ episodes" in five places, using 50+ so it ages better
- Corrects three claims that were also wrong on How It Works: 68 voice profiles
(now 82), adaptive call shapes (deleted system), and 5 recording stems (now 7)
Also adds the Tier 2 plan for crawlable per-episode pages.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The How It Works page still described the show as set in New Mexico and Arizona
and cited stats from the caller generator that was deleted in the redesign.
- Rewrites the geography section around the six Big Bend locales the batch
prompt actually carries, and moves the two caller examples to Marathon and
Alpine
- Replaces the four stat tiles, which counted a name pool, call shapes and
regulars that no longer exist, with figures taken from the current code:
callers per show, usable voice roster, identity fields, named regulars
- Swaps the 70/30 advice split for the real roster mix, drops the "1,000
calling reasons" pool, and replaces the call-shape prose with the
anti-collision rule that superseded it
- Names Silas and Crispin as the regulars instead of Leon and Shaniqua
- Corrects the stem count to seven and control room channels to eight, both
of which gained a Devon track
- Removes two claims that the show tracks energy and pacing at runtime; no
such state exists, so the text now describes the thematic matcher and the
roster mix instead
- Moves the eight homepage testimonials from New Mexico and Arizona towns to
Big Bend, including the two whose quotes named the old location
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Previously _build_backgrounds ran two parallel Sonnet calls of 6
callers each, with zero cross-batch awareness. Each batch independently
gravitated to the same quirky archetypes (Coast to Coast weirdos,
specific hobbies) and produced clustered rosters — one recent show
had two BBQ competitors, two taxidermists, and two conspiracy-pattern
callers across 10 slots.
Collapses to a single Sonnet call generating all 10 at once so the
model can self-diversify across the full roster. The BATCH_SYSTEM_PROMPT
also gets an explicit ANTI-COLLISION RULE with concrete forbidden
examples (no two BBQ competitors, no two taxidermists, no two mystery-
signal callers, etc.) and an instruction to scan output for collisions
before finalizing.
max_tokens bumped 8000 -> 16000 to accommodate the larger response.
Wall-clock cost: ~30s -> ~60s, acceptable for the diversity gain.
Verified on a fresh reset: 10 fully differentiated callers, zero
archetype collisions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Setting a show theme after the caller lineup was loaded caused every
caller button to keep its pre-theme label while the LLM dialog used
the post-theme bg names — host says "Hi Phil", caller replies "it's
Cody, actually". Three underlying bugs, all fixed here:
- _regenerate_backgrounds_for_keys ignored its keys parameter and
replaced the entire caller_backgrounds dict, clobbering used slots
along with unused ones. Now only the listed slots are touched.
- set_show_theme's used_keys detection compared record.caller_name
(the slim bg name) against CALLER_BASES[k]["name"] (the randomized
fallback name) — two different namespaces that never match, so
every slot was flagged unused. Now it matches against
session.caller_backgrounds[k]["name"].
- set_show_theme fired the regeneration as a detached asyncio task,
so the POST returned before bg was consistent. Even if the frontend
did reload /api/callers, it would race the regen. Now awaited.
Frontend setShowTheme/clearShowTheme now call loadCallers() after
the theme POST resolves so the button list actually refreshes, with
a "Regenerating..." button state during the ~30-60s wait.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brings in the Phase 5B caller-generation rewrite:
- Slim two-stage pipeline: Sonnet 4.6 batch identity pregen at session
start, Haiku 4.5 live dialog per turn. Deletes CallerBackground
dataclass, shape/style/voice-matching, per-model routing, preflight UI.
- New caller_gen.py (slim prompt builder) and regulars_v2.py (Obsidian
lore loader for named recurring callers).
- Reworked caller buttons and info panel around the slim background:
identity, situation, signature, secret want. Removes shape badges,
energy dots, emotion info-badges.
- Inworld TTS: emotional_register -> (temperature, speed_adjust) mapping
via _emotional_register_to_params(). applyTextNormalization ON.
- Silas identity/voice leak fix, avatar gender, pre-warm batch gen.
- Archives old regulars to data/regulars.archived.json, adds 10 sample
caller transcripts under docs/samples/.
Post-merge fixes applied during resolution:
- intern.py: kept main's richer new_show() (NEW SHOW history marker,
trim, _save()) and _track_suggestion(); removed the branch's
duplicate minimal new_show() stub.
- tts.py: added 5 voice speed overrides (Graham, Malcolm, Victoria,
Loretta, Marlene) that main had tuned locally. Evelyn deliberately
NOT added to VOICE_PROFILES — she is in BLACKLISTED_VOICES for
unnatural prosody.
- Main's cost dashboard polish, LLM per-model params, and Devon show
context memory from commits c087c03..61b3cba carried through cleanly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds new_show() that inserts a NEW SHOW marker into Devon's history
so he won't reference previous episodes' callers or topics, trims
older history while keeping some long-term memory, and clears
research_cache + pending suggestions. Buffered suggestions are now
tracked in history via _track_suggestion() so Devon won't suggest
the same thing twice. Devon history scoped to current show on ask.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Different models need different tuning for natural conversation:
Qwen gets high frequency penalty to fight repetition loops, Llama
gets warmer temp to reduce terseness, Grok/Mistral/DeepSeek/Kimi get
slightly warmer than Sonnet defaults. Bumps base httpx timeout from
10s to 30s and fallback per-call timeout from 8s to 20s.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds /api/costs/tts endpoint backed by get_tts_providers() in cost_db,
surfaces a Costs link in the control panel header, and small polish
to the costs page HTML/JS.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
session.caller was reading name/voice from CALLER_BASES randomized defaults
instead of the slim bg dict — causing Silas to appear as "Earl" with wrong
voice. Now prefers bg data when populated.
Also: avatar endpoint infers gender from voice pool, /api/callers returns bg
name, regulars get canonical voice force-locked, batch gen splits into two
parallel calls with pre-warm so subsequent resets are instant.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Changes to backend/services/tts.py:
- Add temperature: 0.9 and applyTextNormalization: "ON" to Inworld payload
(text normalization auto-speaks "$5,432", "Dr.", phone numbers, dates, etc.)
- Add _emotional_register_to_params() mapping caller's emotional_register
to (temperature, speed_adjust) across 5 families: sadness/grief,
anger/aggression, manic/excited, nervous/earnest, gruff/restrained.
- generate_speech_inworld() and generate_speech() now accept optional
emotional_register kwarg; all providers take it via the dispatch lambdas
(only inworld uses it; others ignore)
- Clamp speed to 0.5-1.5 after both emotional-register and per-text adjustments
In backend/main.py: plumb emotional_register through the two caller-dialog
TTS calls (auto-respond and ai-respond) by reading from the slim caller
background dict. Devon and cohost/announcer calls pass empty string and
hit the default (0.9, 0.0) branch — no behavior change.
Replace the CallerBackground isinstance branching in _summarize_ai_call
with direct reads from the new slim caller background dict
(emotional_register, situation, specific_details). Decouples CallRecord
population from caller_styles/caller_shapes so those fields can be
deleted in a later commit.
CallRecord schema preserved for backward-compat with existing
session_checkpoint.json. topic_category, emotional_state, energy_level
are now always empty strings (obsolete under slim architecture).
22 tasks across 5 phases: scaffolding, regulars v2, main.py integration
behind feature flag, user validation gate, then deletion of ~2000 lines.
TDD where possible; manual verification of LLM outputs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Session.reset() calls intern_service.new_show() to reset per-show state,
but the method was never defined (added in 376265e without the impl).
Clears pending_interjection, pending_sources, and research_cache.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two-stage architecture: sonnet-4.6 pre-gen batch for rich identities,
haiku-4.5 for live dialog. Replaces static pools + 9-model routing +
scoring/weighting scaffolding. Introduces tiered regulars system with
Silas as canonical (lore in Obsidian vault) and hard-gated arc regulars.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7 new endpoints: summary, timeline, models, categories, sessions,
session detail, expensive calls. All support period filtering.
Page route at /costs serves costs.html before the catch-all.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each LLM/TTS call now writes to both in-memory records and SQLite
via cost_db. Session totals updated on save(). All DB writes wrapped
in try/except to never break live shows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add check_same_thread=False for background thread access
- Add standalone timestamp index for non-session queries
- Join to sessions table for consistent period filtering in get_models/categories/expensive_calls
- Wrap record imports in try/except with .get() for timestamp
- Add avg_cost_per_session to get_summary()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>