primeira versão customizada de VAD- Voice Activity Detection

This commit is contained in:
2026-10-30 19:15:01 +00:00
parent 2449b34137
commit aba05cb826
14 changed files with 3457 additions and 129 deletions

View File

@@ -155,6 +155,20 @@ async def send_json(
)
)
async def close_optional_provider(provider: object | None) -> None:
"""
Close providers exposing an optional asynchronous cleanup hook.
Providers without aclose() require no action.
"""
if provider is None:
return
close_method = getattr(provider, "aclose", None)
if callable(close_method):
await close_method()
async def forward_stt_events(
websocket: WebSocket,
@@ -331,13 +345,29 @@ async def audio_socket(websocket: WebSocket) -> None:
except asyncio.CancelledError:
raise
except Exception:
except Exception as exc:
logger.exception(
"Turn finalization failed. session_id=%s turn_id=%s",
session.session_id,
turn_id,
)
if turn_id not in interrupting_turns:
try:
await send_json(
websocket,
error_event(
"TURN_EXECUTION_FAILED",
str(exc),
session_id=session.session_id,
turn_id=turn_id,
),
)
except Exception:
logger.exception(
"Failed to report turn execution failure."
)
async def cancel_finalization_task() -> None:
"""
Cancel and await the Gateway-owned finalization task.
@@ -792,7 +822,6 @@ async def audio_socket(websocket: WebSocket) -> None:
continue
# ---------------------------------------------------------
# audio.stop
# ---------------------------------------------------------
@@ -805,37 +834,41 @@ async def audio_socket(websocket: WebSocket) -> None:
"STT session has not been started."
)
if pending_audio_chunk is not None:
raise ValueError(
"audio.stop received while an audio chunk is pending."
)
if agent is None:
raise RuntimeError(
"Agent adapter is not initialized."
)
if tts is None:
raise RuntimeError(
"TTS adapter is not initialized."
)
if pipeline is None:
raise RuntimeError(
"Voice pipeline is not initialized."
)
# Finish the STT session normally.
await stt.stop_session()
await pipeline.wait_turn_task()
stt_events_task = None
try:
pipeline.complete_turn(request.turn_id)
except (RuntimeError, ValueError) as exc:
if agent is None or tts is None:
raise RuntimeError(
"Turn pipeline did not complete successfully."
) from exc
"Agent or TTS adapter is not initialized."
)
if not pipeline.is_active_turn(request.turn_id):
raise ValueError(
f"Unexpected turn_id={request.turn_id}."
)
if pending_audio_chunk is not None:
raise ValueError(
"audio.stop received while an audio chunk is pending."
)
if (
finalization_task is not None
and not finalization_task.done()
):
raise RuntimeError(
"Turn finalization is already running."
)
await stt.stop_session()
# Do not await OCI/TTS here.
# Keep the WebSocket receive loop available.
finalization_task = asyncio.create_task(
finalize_turn(request.turn_id)
)
except (ValidationError, RuntimeError, ValueError) as exc:
await send_json(
@@ -847,20 +880,10 @@ async def audio_socket(websocket: WebSocket) -> None:
turn_id=payload.get("turn_id"),
),
)
continue
stt = None
pending_audio_chunk = None
await send_json(
websocket,
TurnEnded(
session_id=session.session_id,
turn_id=request.turn_id,
),
)
continue
# ---------------------------------------------------------
# interrupt
# ---------------------------------------------------------
@@ -895,8 +918,11 @@ async def audio_socket(websocket: WebSocket) -> None:
# Only cancel resources belonging to the requested active turn.
if pipeline.is_active_turn(request.turn_id):
try:
# Cancel and await STT/Agent/TTS execution before
# confirming the interruption to the client.
async with turn_transition_lock:
interrupting_turns.add(request.turn_id)
await cancel_finalization_task()
await pipeline.cancel_turn_task()
# Abort transcription without requesting a final result.
@@ -923,8 +949,19 @@ async def audio_socket(websocket: WebSocket) -> None:
continue
else:
# Preserve the existing tolerant behavior for unknown turns.
pipeline.interrupt_if_active(request.turn_id)
await send_json(
websocket,
error_event(
"INVALID_TURN",
(
f"Turn {request.turn_id} "
"is no longer active."
),
session_id=session.session_id,
turn_id=request.turn_id,
),
)
continue
await send_json(
websocket,
@@ -946,6 +983,12 @@ async def audio_socket(websocket: WebSocket) -> None:
raise RuntimeError(
"Voice pipeline is not initialized."
)
# Prevent normal completion during session shutdown.
async with turn_transition_lock:
if session.active_turn_id is not None:
interrupting_turns.add(session.active_turn_id)
await cancel_finalization_task()
# Cancel and await any running STT/Agent/TTS task.
await pipeline.cancel_turn_task()
@@ -1079,3 +1122,15 @@ async def audio_socket(websocket: WebSocket) -> None:
session.session_id if session else None,
)
# Release provider-owned HTTP connections.
if agent is not None:
try:
await close_optional_provider(agent)
except Exception:
logger.exception(
"Failed to close Agent provider. session_id=%s",
session.session_id if session else None,
)
finally:
agent = None