Devon fixes, theme prompt rewrite, sentence trimmer, cost tracker, normalization

- Fix Devon "if that makes sense" overuse (limit to once per show)
- Suppress Devon failed lookup notifications for self-initiated searches
- Strengthen show theme prompts (2/3 callers call because of theme)
- Fix sentence trimmer splitting on abbreviations (Mr. Mrs. Dr. etc.)
- Fix cost tracker data lost on server restart (persist in checkpoint)
- Ad/ident normalization targets -4dB below dialog for perceived loudness match
- Lower cross-speaker transition threshold to 5s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 03:55:55 -06:00
parent 5d8ab57e20
commit 90e51698b8
3 changed files with 56 additions and 11 deletions
+35 -5
View File
@@ -24,7 +24,7 @@ from .config import settings
from .services.caller_service import CallerService
from .services.transcription import transcribe_audio
from .services.llm import llm_service
from .services.cost_tracker import cost_tracker
from .services.cost_tracker import cost_tracker, LLMCallRecord, TTSCallRecord
from .services.tts import generate_speech
from .services.audio import audio_service
from .services.stem_recorder import StemRecorder
@@ -5314,7 +5314,10 @@ TIME: {time_ctx} {season_ctx}
{fluency_hint}
{f'SOME DETAILS ABOUT THEM: {seed_text}' if seed_text else ''}
{f'CALLER ENERGY: {style_hint}' if style_hint else ''}
{f"SHOW THEME: Tonight's show theme is '{session.show_theme}'. This caller might have a story or angle related to this theme — or they might not. Not every caller has to be about the theme, but if their reason for calling can naturally connect to it, lean into that connection. The theme should feel like a through-line, not a mandate." if session.show_theme else ''}
{f"""SHOW THEME: Tonight's show theme is '{session.show_theme}'.
Most callers tonight are calling BECAUSE of the theme they heard the host announce it and thought "oh man, I have a story for this." Their reason for calling should be genuinely, specifically connected to the theme. Not a surface-level mention the theme should be woven into WHY they picked up the phone. Maybe the theme hit a nerve, maybe it reminded them of something wild that happened, maybe they have a hot take or a confession related to it.
About 1 in 3 callers can be unrelated to the theme they just have their own thing going on and called regardless. But the majority should feel like the theme drew them in.
When the theme connects, make it SPECIFIC not "oh yeah I have a story about that" but a concrete situation that naturally ties to '{session.show_theme}'.""" if session.show_theme else ''}
Respond with a JSON object containing these fields:
@@ -6017,7 +6020,7 @@ def get_caller_prompt(caller: dict, show_history: str = "",
theme_context = ""
if session.show_theme:
theme_context = f"\nSHOW THEME: Tonight's show theme is \"{session.show_theme}\". You're aware of the theme — the host mentioned it at the top of the show. If your story or situation connects to it, you might bring it up naturally. But don't force it. Not every caller has to be about the theme. If the host steers you toward the theme, go with it.\n"
theme_context = f"""\nSHOW THEME: Tonight's theme is \"{session.show_theme}\". If your story connects to this theme, OWN IT — you called because you heard the theme and knew you had to share. Mention the theme connection early, be enthusiastic about it. You're not just aware of the theme, you're excited that it's YOUR night to call. If the host brings up the theme, engage with energy. If your story doesn't relate to the theme, that's fine — just be yourself and tell your story.\n"""
now = datetime.now(_MST)
date_str = now.strftime("%A, %B %d")
@@ -6592,6 +6595,10 @@ def _save_checkpoint():
"relationship_context": session.relationship_context,
"intern_monitoring": session.intern_monitoring,
"costs": cost_tracker.get_live_summary(),
"cost_records": {
"llm": [asdict(r) for r in cost_tracker.llm_records],
"tts": [asdict(r) for r in cost_tracker.tts_records],
},
"saved_at": time.time(),
}
with open(CHECKPOINT_FILE, "w") as f:
@@ -6639,6 +6646,28 @@ def _load_checkpoint() -> bool:
CALLER_BASES[key]["voice"] = snapshot["voice"]
CALLER_BASES[key]["returning"] = snapshot.get("returning", False)
CALLER_BASES[key]["regular_id"] = snapshot.get("regular_id")
# Restore cost tracker records
cost_records = data.get("cost_records", {})
if cost_records:
cost_tracker.reset()
for r in cost_records.get("llm", []):
cost_tracker.llm_records.append(LLMCallRecord(**r))
for r in cost_records.get("tts", []):
cost_tracker.tts_records.append(TTSCallRecord(**r))
# Rebuild running totals from restored records
for r in cost_tracker.llm_records:
cost_tracker._llm_cost += r.cost_usd
cost_tracker._llm_calls += 1
cost_tracker._prompt_tokens += r.prompt_tokens
cost_tracker._completion_tokens += r.completion_tokens
cost_tracker._total_tokens += r.total_tokens
cat = cost_tracker._by_category.setdefault(r.category, {"cost": 0.0, "calls": 0, "tokens": 0})
cat["cost"] += r.cost_usd
cat["calls"] += 1
cat["tokens"] += r.total_tokens
for r in cost_tracker.tts_records:
cost_tracker._tts_cost += r.cost_usd
print(f"[Checkpoint] Restored {len(cost_tracker.llm_records)} LLM + {len(cost_tracker.tts_records)} TTS cost records")
mins = age / 60
print(f"[Checkpoint] Restored session {session.id} ({len(session.call_history)} calls, {mins:.0f}m old)")
return True
@@ -8026,8 +8055,9 @@ def _trim_to_sentences(text: str, max_sentences: int) -> str:
"""Hard-trim response to at most max_sentences sentences."""
if not text:
return text
# Split on sentence-ending punctuation, keeping the delimiter
parts = re.split(r'(?<=[.!?])\s+', text.strip())
# Split on sentence-ending punctuation, keeping the delimiter.
# Negative lookbehind avoids splitting on common abbreviations (Mr. Mrs. Ms. Dr. St. etc.)
parts = re.split(r'(?<!Mr)(?<!Mrs)(?<!Ms)(?<!Dr)(?<!St)(?<!Jr)(?<!Sr)(?<!vs)(?<![A-Z])(?<=[.!?])\s+', text.strip())
if len(parts) <= max_sentences:
return text
trimmed = ' '.join(parts[:max_sentences])
+12 -2
View File
@@ -26,7 +26,7 @@ YOUR BACKGROUND: Communications degree from NMSU. This is your first day on the
YOUR PERSONALITY:
- You are a weird little dude. Kinda creepy, very funny, awkward, and surprisingly sharp. You give off a vibe that something is slightly off about you but people can't quite place it. But underneath it all, you are genuinely lovable. You have a good heart. You root for people. You get excited for callers. You care about the show. People should hear you and think "this guy is insane" and also "I love this guy." You are the kind of person who is impossible not to root for even when you're being deeply strange.
- You overshare constantly. You'll drop deeply personal, uncomfortably specific details about your life — sexual history, bizarre habits, unsettling childhood memories — and then keep going like nothing happened. You are genuinely vulnerable and honest about the deepest, weirdest parts of yourself. You don't do this for shock value. You just have no filter and no shame. This vulnerability is what makes you endearing — you're not performing, you're just being yourself, and yourself happens to be a lot.
- You start explanations with "So basically..." and end them with "...if that makes sense."
- You start explanations with "So basically..." and occasionally end them with "...if that makes sense." Use that phrase sparingly — once per show at most, not every response.
- You say "actually" when correcting things. You use "per se" slightly wrong. You say "ironically" about things that are not ironic.
- You are NOT a comedian. You are funny because you are sincere, specific, and deeply strange. You state disturbing or absurd things with complete seriousness. You have strong opinions about low-stakes things. You occasionally say something devastating without realizing it.
- When you accidentally reveal something dark or sad, you move past it immediately like it's nothing. "Yeah, my landlord's selling the building so I might have to — anyway, it says here that..."
@@ -52,7 +52,8 @@ HOW YOU INTERJECT:
WHEN LUKE ASKS YOU TO LOOK SOMETHING UP:
- Respond like you're already doing it: "Yeah, one sec..." or "Pulling that up..."
- Deliver the info slightly too formally, like you're reading. Then rephrase in normal language if Luke seems confused.
- If you can't find it or don't know: say so. "I'm not finding anything on that" or "I don't actually know." You do not bluff.
- If you can't find it or don't know and Luke ASKED you directly: say so briefly. "I'm not finding anything on that" or "I don't actually know." You do not bluff.
- If you looked something up on your own (monitoring, interjecting) and couldn't find anything: just stay quiet. Do NOT announce failed lookups. Nobody wants to hear "I looked for X but couldn't find anything." If you have nothing useful, say nothing.
- Occasionally you already know the answer because you looked it up before being asked. This is one of your best qualities.
WHAT YOU KNOW:
@@ -447,6 +448,15 @@ class InternService:
if not text or "NOTHING_TO_ADD" in text:
return None
# Suppress interjections that are just announcing failed lookups
failed_phrases = ["couldn't find", "could not find", "not finding anything",
"no results", "didn't find", "wasn't able to find",
"couldn't locate", "no information on"]
text_lower = text.lower()
if any(phrase in text_lower for phrase in failed_phrases):
print(f"[Intern] Suppressed failed-lookup interjection: {text[:60]}...")
return None
if tool_calls:
entry = {
"question": "(interjection)",
+9 -4
View File
@@ -769,27 +769,32 @@ local function phase2_normalize(dialog_regions, ad_regions, ident_regions, dialo
end
log("Phase 2: Dialog RMS = " .. string.format("%.1f", dialog_rms_db) .. " dBFS")
local dialog_db = dialog_rms_db
-- Ads/idents are pre-compressed dense audio, so they sound louder than dialog
-- at the same RMS. Target a few dB below dialog to match perceived loudness.
local AD_IDENT_OFFSET_DB = -4
local ad_ident_target = dialog_rms_db + AD_IDENT_OFFSET_DB
log("Phase 2: AD/IDENT target = " .. string.format("%.1f", ad_ident_target) .. " dBFS (" .. AD_IDENT_OFFSET_DB .. "dB offset from dialog)")
if #ad_regions > 0 then
progress_detail = "Ads"
coroutine.yield()
log("Phase 2: Normalizing " .. #ad_regions .. " AD region(s)...")
normalize_track_regions(ADS_TRACK, ad_regions, dialog_db)
normalize_track_regions(ADS_TRACK, ad_regions, ad_ident_target)
end
if #ident_regions > 0 then
progress_detail = "Idents"
progress_pct = 0.33
coroutine.yield()
log("Phase 2: Normalizing " .. #ident_regions .. " IDENT region(s)...")
normalize_track_regions(IDENTS_TRACK, ident_regions, dialog_db)
normalize_track_regions(IDENTS_TRACK, ident_regions, ad_ident_target)
end
progress_detail = "Music"
progress_pct = 0.66
coroutine.yield()
log("Phase 2: Normalizing music track...")
normalize_music_track(dialog_regions, dialog_db)
normalize_music_track(dialog_regions, dialog_rms_db)
progress_pct = 1.0
end