Add BunnyCDN integration, on-air website badge, publish script fixes

- On-air toggle uploads status.json to BunnyCDN + purges cache, website
  polls it every 15s to show live ON AIR / OFF AIR badge
- Publish script downloads Castopod's copy of audio for CDN upload
  (byte-exact match), removes broken slug fallback, syncs all episode
  media to CDN after publishing
- Fix f-string syntax error in publish_episode.py (Python <3.12)
- Enable CORS on BunnyCDN pull zone for json files
- CDN URLs for website OG images, stem recorder bug fixes, LLM token
  budget tweaks, session context in CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 17:34:18 -07:00
parent 7d88c76f90
commit 7b7f9b8208
11 changed files with 454 additions and 61 deletions

View File

@@ -80,6 +80,7 @@ class AudioService:
# Stem recording (opt-in, attached via API)
self.stem_recorder = None
self._stem_mic_stream: Optional[sd.InputStream] = None
# Load saved settings
self._load_settings()
@@ -282,6 +283,8 @@ class AudioService:
stream_ready.set()
if self._recording:
self._recorded_audio.append(indata[:, record_channel].copy())
if self.stem_recorder:
self.stem_recorder.write("host", indata[:, record_channel].copy(), device_sr)
print(f"Recording: opening stream on device {self.input_device} ch {self.input_channel} @ {device_sr}Hz ({max_channels} ch)")
@@ -360,7 +363,7 @@ class AudioService:
# Stem recording: caller TTS
if self.stem_recorder:
self.stem_recorder.write("caller", audio.copy(), device_sr)
self.stem_recorder.write_sporadic("caller", audio.copy(), device_sr)
# Create multi-channel output with audio only on target channel
multi_ch = np.zeros((len(audio), num_channels), dtype=np.float32)
@@ -500,7 +503,7 @@ class AudioService:
# Stem recording: live caller
if self.stem_recorder:
self.stem_recorder.write("caller", audio.copy(), device_sr)
self.stem_recorder.write_sporadic("caller", audio.copy(), device_sr)
if self._live_caller_write:
self._live_caller_write(audio)
@@ -930,7 +933,7 @@ class AudioService:
# Stem recording: sfx
if self.stem_recorder:
self.stem_recorder.write("sfx", audio.copy(), device_sr)
self.stem_recorder.write_sporadic("sfx", audio.copy(), device_sr)
multi_ch = np.zeros((len(audio), num_channels), dtype=np.float32)
multi_ch[:, channel_idx] = audio
@@ -950,6 +953,45 @@ class AudioService:
except Exception as e:
print(f"SFX playback error: {e}")
# --- Stem Mic Capture ---
def start_stem_mic(self):
"""Start a persistent mic capture stream for stem recording.
Runs independently of push-to-talk and host streaming."""
if self._stem_mic_stream is not None:
return
if self.input_device is None:
print("[StemRecorder] No input device configured, skipping host mic capture")
return
device_info = sd.query_devices(self.input_device)
max_channels = device_info['max_input_channels']
device_sr = int(device_info['default_samplerate'])
record_channel = min(self.input_channel, max_channels) - 1
def callback(indata, frames, time_info, status):
if self.stem_recorder:
self.stem_recorder.write("host", indata[:, record_channel].copy(), device_sr)
self._stem_mic_stream = sd.InputStream(
device=self.input_device,
channels=max_channels,
samplerate=device_sr,
dtype=np.float32,
blocksize=1024,
callback=callback,
)
self._stem_mic_stream.start()
print(f"[StemRecorder] Host mic capture started (device {self.input_device} ch {self.input_channel} @ {device_sr}Hz)")
def stop_stem_mic(self):
"""Stop the persistent stem mic capture."""
if self._stem_mic_stream:
self._stem_mic_stream.stop()
self._stem_mic_stream.close()
self._stem_mic_stream = None
print("[StemRecorder] Host mic capture stopped")
# Global instance
audio_service = AudioService()