Add continuous host mic streaming to real callers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 15:51:17 -07:00
parent bf140a77b7
commit 863a81f87b
2 changed files with 84 additions and 0 deletions

View File

@@ -863,6 +863,29 @@ async def caller_audio_stream(websocket: WebSocket):
)
# --- Host Audio Broadcast ---
async def _broadcast_host_audio(pcm_bytes: bytes):
"""Send host mic audio to all active real callers"""
for caller_id in list(caller_service.active_calls.keys()):
try:
await caller_service.send_audio_to_caller(caller_id, pcm_bytes, 16000)
except Exception:
pass
def _host_audio_sync_callback(pcm_bytes: bytes):
"""Sync wrapper to schedule async broadcast from audio thread"""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
loop.call_soon_threadsafe(
asyncio.ensure_future, _broadcast_host_audio(pcm_bytes)
)
except Exception:
pass
# --- Queue Endpoints ---
@app.get("/api/queue")
@@ -888,6 +911,10 @@ async def take_call_from_queue(caller_id: str):
# Notify caller they're on air via WebSocket
await caller_service.notify_caller(caller_id, {"status": "on_air", "channel": call_info["channel"]})
# Start host mic streaming if this is the first real caller
if len(caller_service.active_calls) == 1:
audio_service.start_host_stream(_host_audio_sync_callback)
return {
"status": "on_air",
"caller": call_info,
@@ -1009,6 +1036,10 @@ async def hangup_real_caller():
caller_service.hangup(caller_id)
await caller_service.disconnect_caller(caller_id)
# Stop host streaming if no more active callers
if len(caller_service.active_calls) == 0:
audio_service.stop_host_stream()
session.active_real_caller = None
# Play hangup sound

View File

@@ -48,6 +48,10 @@ class AudioService:
self._caller_stop_event = threading.Event()
self._caller_thread: Optional[threading.Thread] = None
# Host mic streaming state
self._host_stream: Optional[sd.InputStream] = None
self._host_send_callback: Optional[Callable] = None
# Sample rates
self.input_sample_rate = 16000 # For Whisper
self.output_sample_rate = 24000 # For TTS
@@ -345,6 +349,55 @@ class AudioService:
except Exception as e:
print(f"Real caller audio routing error: {e}")
# --- Host Mic Streaming ---
def start_host_stream(self, send_callback: Callable):
"""Start continuous host mic capture for streaming to real callers"""
if self.input_device is None:
print("[Audio] No input device configured for host streaming")
return
self._host_send_callback = send_callback
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
import librosa
def callback(indata, frames, time_info, status):
if not self._host_send_callback:
return
# Extract the configured input channel
mono = indata[:, record_channel].copy()
# Resample to 16kHz if needed
if device_sr != 16000:
mono = librosa.resample(mono, orig_sr=device_sr, target_sr=16000)
# Convert float32 to int16 PCM
pcm = (mono * 32767).astype(np.int16).tobytes()
self._host_send_callback(pcm)
self._host_stream = sd.InputStream(
device=self.input_device,
channels=max_channels,
samplerate=device_sr,
dtype=np.float32,
blocksize=4096,
callback=callback,
)
self._host_stream.start()
print(f"[Audio] Host mic streaming started (device {self.input_device} ch {self.input_channel} @ {device_sr}Hz)")
def stop_host_stream(self):
"""Stop host mic streaming"""
if self._host_stream:
self._host_stream.stop()
self._host_stream.close()
self._host_stream = None
self._host_send_callback = None
print("[Audio] Host mic streaming stopped")
# --- Music Playback ---
def load_music(self, file_path: str) -> bool: