Add continuous host mic streaming to real callers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 ---
|
# --- Queue Endpoints ---
|
||||||
|
|
||||||
@app.get("/api/queue")
|
@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
|
# Notify caller they're on air via WebSocket
|
||||||
await caller_service.notify_caller(caller_id, {"status": "on_air", "channel": call_info["channel"]})
|
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 {
|
return {
|
||||||
"status": "on_air",
|
"status": "on_air",
|
||||||
"caller": call_info,
|
"caller": call_info,
|
||||||
@@ -1009,6 +1036,10 @@ async def hangup_real_caller():
|
|||||||
caller_service.hangup(caller_id)
|
caller_service.hangup(caller_id)
|
||||||
await caller_service.disconnect_caller(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
|
session.active_real_caller = None
|
||||||
|
|
||||||
# Play hangup sound
|
# Play hangup sound
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ class AudioService:
|
|||||||
self._caller_stop_event = threading.Event()
|
self._caller_stop_event = threading.Event()
|
||||||
self._caller_thread: Optional[threading.Thread] = None
|
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
|
# Sample rates
|
||||||
self.input_sample_rate = 16000 # For Whisper
|
self.input_sample_rate = 16000 # For Whisper
|
||||||
self.output_sample_rate = 24000 # For TTS
|
self.output_sample_rate = 24000 # For TTS
|
||||||
@@ -345,6 +349,55 @@ class AudioService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Real caller audio routing error: {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 ---
|
# --- Music Playback ---
|
||||||
|
|
||||||
def load_music(self, file_path: str) -> bool:
|
def load_music(self, file_path: str) -> bool:
|
||||||
|
|||||||
Reference in New Issue
Block a user