fix(voice): preserve pending message intent across reconnects

This commit is contained in:
stu2not
2026-10-30 16:46:55 +08:00
parent 9ebb1a78ec
commit 475a2447d1
4 changed files with 290 additions and 33 deletions
@@ -176,15 +176,13 @@ async def handle_binary_message(websocket: WebSocket, device_id: str, serial_num
return current_active_session, first_audio_received_time
session.start_received = True
session.voice_target = await device_target_cache.claim_target(device_id, websocket, session_id)
if session.voice_target:
# Acknowledge START now; the first audio packet selects AI (0) or
# message (4). A pending message must not swallow an AI recording.
await websocket.send_text("START_ACK")
session.start_ack_sent = True
return session_key, None
current_active_session = await start_ai_recording(
websocket, device_id, session_id, session_key, session, current_active_session, sample_rate
)
# START does not identify AI versus message audio. Decide on the first
# audio packet, even without a target, so an invalid message cannot
# start ASR and later report an unrelated recognition failure.
current_active_session = await handle_start_packet(
device_id, session_id, session_key, session, current_active_session)
await websocket.send_text("START_ACK")
session.start_ack_sent = True
return current_active_session, None
if not session.start_received or session.recording_finished:
@@ -209,26 +207,32 @@ async def handle_binary_message(websocket: WebSocket, device_id: str, serial_num
session_logger.info(device_id, session_id, f"收到第一个音频数据包时间: {first_audio_received_time}")
elif packet_type == 4:
target = session.voice_target
if target and not session.ai_started:
if await handle_target_audio_packet(device_id, audio_data, target, websocket, session_id):
session.message_audio_received = True
if not session.ai_started:
session.message_audio_received = True
if target:
await handle_target_audio_packet(device_id, audio_data, target, websocket, session_id)
else:
session_logger.warning(device_id, session_id, "留言录音没有对应目标,将返回留言失败提示")
else:
session_logger.warning(device_id, session_id, "忽略没有对应留言目标的留言音频包")
session_logger.warning(device_id, session_id, "忽略普通对话中混入的留言音频")
return current_active_session, first_audio_received_time
elif packet_type == 3:
if session.voice_target:
await device_target_cache.remove_target(device_id, expected=session.voice_target, reason="interrupted")
if not session.ai_started:
if session.voice_target:
await device_target_cache.remove_target(device_id, expected=session.voice_target, reason="interrupted")
session.voice_target = None
session.recording_finished = True
await websocket.send_text("INTERRUPT_ACK")
await websocket.send_text("INTERRUPT_PROCESSED")
await interrupt_handler.remove_session(session_key)
return None if current_active_session == session_key else current_active_session, None
current_active_session = await handle_interrupt_packet(
device_id, session_id, session_key, session, current_active_session
)
elif packet_type == 2:
if session.voice_target:
target = await device_target_cache.take_target(device_id, session.voice_target, websocket, session_id)
if session.voice_target or session.message_audio_received:
target = (await device_target_cache.take_target(device_id, session.voice_target, websocket, session_id)
if session.voice_target else None)
session.voice_target = None
session.recording_finished = True
try:
@@ -240,10 +244,19 @@ async def handle_binary_message(websocket: WebSocket, device_id: str, serial_num
else:
await process_cached_audio(device_id, target.target_device_id, serial_number,
audio_cache_key=target.audio_cache_key)
elif current_active_session is None or current_active_session == session_key:
await websocket.send_text(
f"PROMPT_SOUND_URL:http://{settings.server_host}:{settings.server_port}/assets/audio/save_audio_fail.mp3")
finally:
if target:
await target_audio_cache.clear_audio_data(target.audio_cache_key)
await interrupt_handler.remove_session(session_key)
return None if current_active_session == session_key else current_active_session, None
if not session.ai_started:
# Preserve the existing silent-recording response for START/END
# without audio while keeping unowned messages out of the AI path.
current_active_session = await start_ai_recording(
websocket, device_id, session_id, session_key, session, current_active_session, sample_rate)
if current_active_session == session_key:
current_active_session = None
@@ -254,9 +267,6 @@ async def handle_binary_message(websocket: WebSocket, device_id: str, serial_num
async def start_ai_recording(websocket, device_id, session_id, session_key, session, current_active_session, sample_rate):
current_active_session = await handle_start_packet(
device_id, session_id, session_key, session, current_active_session
)
await handle_websocket_data(websocket, session_key, session, 1, b"", sample_rate)
session.ai_started = True
return current_active_session