Add persistent caller voices, Discord, REC/on-air linking, SEO fixes, ep9
- Returning callers now keep their voice across sessions (stored in regulars.json) - Backfilled voice assignments for all 11 existing regulars - Discord button on homepage + link in all page footers - REC and On-Air buttons now toggle together (both directions) - Fixed host mic double-stream bug (stem_mic vs host_stream conflict) - SEO: JSON-LD structured data on episode + how-it-works pages - SEO: noscript fallbacks, RSS links, twitter meta tags - Episode 9 transcript and sitemap update Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -158,7 +158,9 @@ def _randomize_callers():
|
||||
base["name"] = regular["name"]
|
||||
base["returning"] = True
|
||||
base["regular_id"] = regular["id"]
|
||||
# Keep the randomly assigned voice — regulars sound different each time
|
||||
# Restore their stored voice so they sound the same every time
|
||||
if regular.get("voice"):
|
||||
base["voice"] = regular["voice"]
|
||||
if returning:
|
||||
names = [r["name"] for r in returning]
|
||||
print(f"[Regulars] Injected returning callers: {', '.join(names)}")
|
||||
@@ -2335,21 +2337,64 @@ def _update_on_air_cdn(on_air: bool):
|
||||
|
||||
@app.post("/api/on-air")
|
||||
async def set_on_air(state: dict):
|
||||
"""Toggle whether the show is on air (accepting phone calls)"""
|
||||
"""Toggle whether the show is on air (accepting phone calls). Also toggles recording."""
|
||||
global _show_on_air
|
||||
_show_on_air = bool(state.get("on_air", False))
|
||||
print(f"[Show] On-air: {_show_on_air}")
|
||||
if _show_on_air:
|
||||
# Auto-start recording FIRST (before host stream, which takes over mic capture)
|
||||
if audio_service.stem_recorder is None:
|
||||
try:
|
||||
from datetime import datetime
|
||||
dir_name = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
||||
recordings_dir = Path("recordings") / dir_name
|
||||
import sounddevice as sd
|
||||
device_info = sd.query_devices(audio_service.output_device) if audio_service.output_device is not None else None
|
||||
sr = int(device_info["default_samplerate"]) if device_info else 48000
|
||||
recorder = StemRecorder(recordings_dir, sample_rate=sr)
|
||||
recorder.start()
|
||||
audio_service.stem_recorder = recorder
|
||||
audio_service.start_stem_mic()
|
||||
add_log(f"Stem recording auto-started -> {recordings_dir}")
|
||||
except Exception as e:
|
||||
print(f"[Show] Failed to auto-start recording: {e}")
|
||||
_start_host_audio_sender()
|
||||
# Host stream takes over mic capture (closes stem_mic if active)
|
||||
audio_service.start_host_stream(_host_audio_sync_callback)
|
||||
else:
|
||||
audio_service.stop_host_stream()
|
||||
# Auto-stop recording
|
||||
if audio_service.stem_recorder is not None:
|
||||
try:
|
||||
audio_service.stop_stem_mic()
|
||||
stems_dir = audio_service.stem_recorder.output_dir
|
||||
paths = audio_service.stem_recorder.stop()
|
||||
audio_service.stem_recorder = None
|
||||
add_log(f"Stem recording auto-stopped. Running post-production...")
|
||||
import subprocess, sys
|
||||
python = sys.executable
|
||||
output_file = stems_dir / "episode.mp3"
|
||||
def _run_postprod():
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[python, "postprod.py", str(stems_dir), "-o", str(output_file)],
|
||||
capture_output=True, text=True, timeout=300,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
add_log(f"Post-production complete -> {output_file}")
|
||||
else:
|
||||
add_log(f"Post-production failed: {result.stderr[:300]}")
|
||||
except Exception as e:
|
||||
add_log(f"Post-production error: {e}")
|
||||
threading.Thread(target=_run_postprod, daemon=True).start()
|
||||
except Exception as e:
|
||||
print(f"[Show] Failed to auto-stop recording: {e}")
|
||||
threading.Thread(target=_update_on_air_cdn, args=(_show_on_air,), daemon=True).start()
|
||||
return {"on_air": _show_on_air}
|
||||
return {"on_air": _show_on_air, "recording": audio_service.stem_recorder is not None}
|
||||
|
||||
@app.get("/api/on-air")
|
||||
async def get_on_air():
|
||||
return {"on_air": _show_on_air}
|
||||
return {"on_air": _show_on_air, "recording": audio_service.stem_recorder is not None}
|
||||
|
||||
|
||||
# --- SignalWire Endpoints ---
|
||||
@@ -2654,6 +2699,7 @@ async def _summarize_ai_call(caller_key: str, caller_name: str, conversation: li
|
||||
location="in " + job_parts[1].strip() if isinstance(job_parts, tuple) and len(job_parts) > 1 else "unknown",
|
||||
personality_traits=traits[:4],
|
||||
first_call_summary=summary,
|
||||
voice=base.get("voice"),
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[Regulars] Promotion logic error: {e}")
|
||||
@@ -3896,7 +3942,15 @@ async def start_stem_recording():
|
||||
audio_service.stem_recorder = recorder
|
||||
audio_service.start_stem_mic()
|
||||
add_log(f"Stem recording started -> {recordings_dir}")
|
||||
return {"status": "recording", "dir": str(recordings_dir)}
|
||||
# Auto go on-air
|
||||
global _show_on_air
|
||||
if not _show_on_air:
|
||||
_show_on_air = True
|
||||
_start_host_audio_sender()
|
||||
audio_service.start_host_stream(_host_audio_sync_callback)
|
||||
threading.Thread(target=_update_on_air_cdn, args=(True,), daemon=True).start()
|
||||
add_log("Show auto-set to ON AIR")
|
||||
return {"status": "recording", "dir": str(recordings_dir), "on_air": _show_on_air}
|
||||
|
||||
|
||||
@app.post("/api/recording/stop")
|
||||
@@ -3909,6 +3963,14 @@ async def stop_stem_recording():
|
||||
audio_service.stem_recorder = None
|
||||
add_log(f"Stem recording stopped. Running post-production...")
|
||||
|
||||
# Auto go off-air
|
||||
global _show_on_air
|
||||
if _show_on_air:
|
||||
_show_on_air = False
|
||||
audio_service.stop_host_stream()
|
||||
threading.Thread(target=_update_on_air_cdn, args=(False,), daemon=True).start()
|
||||
add_log("Show auto-set to OFF AIR")
|
||||
|
||||
# Auto-run postprod in background
|
||||
import subprocess, sys
|
||||
python = sys.executable
|
||||
@@ -3927,7 +3989,7 @@ async def stop_stem_recording():
|
||||
add_log(f"Post-production error: {e}")
|
||||
|
||||
threading.Thread(target=_run_postprod, daemon=True).start()
|
||||
return {"status": "stopped", "stems": paths, "processing": str(output_file)}
|
||||
return {"status": "stopped", "stems": paths, "processing": str(output_file), "on_air": _show_on_air}
|
||||
|
||||
|
||||
@app.post("/api/recording/process")
|
||||
|
||||
@@ -522,6 +522,13 @@ class AudioService:
|
||||
print("[Audio] No input device configured for host streaming")
|
||||
return
|
||||
|
||||
# Close stem_mic if active — this stream's callback handles stem recording too
|
||||
if self._stem_mic_stream is not None:
|
||||
self._stem_mic_stream.stop()
|
||||
self._stem_mic_stream.close()
|
||||
self._stem_mic_stream = None
|
||||
print("[Audio] Closed stem_mic (host stream takes over)")
|
||||
|
||||
self._host_send_callback = send_callback
|
||||
|
||||
def _start():
|
||||
@@ -960,9 +967,12 @@ class AudioService:
|
||||
|
||||
def start_stem_mic(self):
|
||||
"""Start a persistent mic capture stream for stem recording.
|
||||
Runs independently of push-to-talk and host streaming."""
|
||||
Skips if _host_stream is already active (it writes to the host stem too)."""
|
||||
if self._stem_mic_stream is not None:
|
||||
return
|
||||
if self._host_stream is not None:
|
||||
print("[StemRecorder] Host stream already capturing mic, skipping stem_mic")
|
||||
return
|
||||
if self.input_device is None:
|
||||
print("[StemRecorder] No input device configured, skipping host mic capture")
|
||||
return
|
||||
|
||||
@@ -51,7 +51,7 @@ class RegularCallerService:
|
||||
|
||||
def add_regular(self, name: str, gender: str, age: int, job: str,
|
||||
location: str, personality_traits: list[str],
|
||||
first_call_summary: str) -> dict:
|
||||
first_call_summary: str, voice: str = None) -> dict:
|
||||
"""Promote a first-time caller to regular"""
|
||||
# Retire oldest if at cap
|
||||
if len(self._regulars) >= MAX_REGULARS:
|
||||
@@ -67,6 +67,7 @@ class RegularCallerService:
|
||||
"job": job,
|
||||
"location": location,
|
||||
"personality_traits": personality_traits,
|
||||
"voice": voice,
|
||||
"call_history": [
|
||||
{"summary": first_call_summary, "timestamp": time.time()}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user