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>
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>
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).
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>
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>
Ep47 post-mortem: fixed theme ignored by callers (backgrounds now
regenerate when theme is set), style-to-model race condition (fallback
to sonnet instead of pool[0]), removed bad pronunciation fixes, added
age-awareness to voice matching, raised MIN_RESPONSE_WORDS to 50.
Swapped problematic model mappings: conspiracy→qwen, know_it_all→mistral,
quiet_nervous→llama, emotional→kimi.
Added GET /api/show/preflight endpoint with 4 checks: model diversity,
theme penetration, voice-age alignment, response coherence (2-exchange
simulation of all callers). Frontend preflight modal with expandable
check cards.
Fixed active caller button not highlighting (moved highlight code before
potentially-failing caller info panel code).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Devon: more conversational when addressed directly (500 tokens, 3-5 sentences)
- Devon: monitor prompt rewritten to encourage more contributions
- Devon: polling interval 15s (was 30s), removed 2-message minimum
- Whisper: no fuzzy name matching for 3-char names, require first letter match
- fetch_music.py: post-fetch vocal detection filter using musicinfo tags
- scan_music_vocals.py: new script to scan existing library for vocal tracks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove caller names from Whisper hint (was corrupting transcriptions)
- Background gen switched to Claude Sonnet 4.6 (cheap models = thin backgrounds)
- "WHAT MAKES A GOOD CALLER" rewritten with concrete examples
- Grok guardrails loosened (were cutting too much edge)
- Response length guidance added to caller prompt
- Retry under-20-word responses once for more detail
- Devon monitor softened from "default silence" to balanced
- Ban stalling phrases: "where was I", "as I was saying", etc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 2+ requirement created a catch-22: regulars couldn't return because they
needed 2 calls, but couldn't get a second call without returning. Dynamic
count already prevents flooding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Only inject 2 returners if pool has 3+ eligible (so it's not the same every show).
With 2 eligible, inject 1. With 1 or 0, inject none.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Style-matched defaults:
- Grok 4.1 Fast for edgy callers (high_energy, confrontational, comedian etc.)
- Claude Sonnet 4.6 for emotional callers (quiet_nervous, sweet_earnest, emotional)
- Mistral Large 2512 for deadpan/mysterious/world-weary
- DeepSeek R1 Distill for storyteller/oversharer/conspiracy/rambler
- Gemini 2.5 Flash for know_it_all
- Llama 3.3 70B for first_time/reluctant callers
Category routing: Grok 4.1 Fast for dialog/devon/backgrounds, Gemini Flash for monitor/summary
Updated OPENROUTER_MODELS and OPENROUTER_PRICING with all new models
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Devon audio independent of caller hangup (separate stop events)
- Personal anecdotes capped at ~30% of responses (was every time)
- Interjection criteria tightened ("default is silence")
- Devon sees his own recent history to avoid repeating info
- Response variety: permits minimal reactions, confusion, silence
- Monitor prompt rewritten to be gatekeeping, not encouraging
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Avatar prefetch checks gender marker, re-fetches on mismatch
- Returning callers need 2+ actual calls before re-eligible (was 1)
- Promotion rate lowered 10% → 5% to prevent pool flooding
- Callback injection skipped for returning callers (already have context)
- Show history clarifies "you are NOT that caller" to prevent identity confusion
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Three strategies: single model, cycle through pool, style-matched
- 18 communication styles mapped to 7 models (Grok, Sonnet, Mistral, Qwen, DeepSeek, Gemini, Llama)
- Per-caller model locked for entire call, overridable mid-show
- Model badges on caller buttons and info panel
- Settings UI for strategy, pool, style mapping, fallback
- Fallback to Sonnet on model failure
- 6 new models added to pricing and dropdown
- Checkpoint persistence for all model state
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>