fix(gateway): transcribe clarify voice replies

This commit is contained in:
izumi0uu 2026-06-26 18:11:11 +08:00 committed by Teknium
parent b8c38a451a
commit aa40f16d3e
2 changed files with 98 additions and 2 deletions

View file

@ -11198,7 +11198,16 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
except Exception:
_pending_clarify = None
if _pending_clarify is not None and _clarify_mod is not None:
_raw_clarify_reply = (event.text or "").strip()
_clarify_has_audio = bool(self._pending_event_audio_paths(event))
_raw_clarify_reply = await self._prepare_clarify_reply_text(event)
if _clarify_has_audio and not _raw_clarify_reply:
logger.info(
"Gateway retained pending clarify after voice transcription "
"produced no usable text (session=%s, id=%s)",
_quick_key,
_pending_clarify.clarify_id,
)
return ""
# Skip slash commands — the user clearly wanted to issue a
# command, not answer the clarify. Leave the clarify pending
# so the user can retry; if it times out, the agent unblocks
@ -13027,6 +13036,20 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
session_key=session_key,
)
async def _prepare_clarify_reply_text(self, event) -> str:
"""Return raw text or successful voice transcripts for a clarify reply."""
if not self._pending_event_audio_paths(event):
return (event.text or "").strip()
_, successful_transcripts = await self._transcribe_pending_audio_event_once(
event, "",
)
return "\n\n".join(
transcript.strip()
for transcript in successful_transcripts
if transcript.strip()
)
def _consume_pending_native_image_paths(self, session_key: str) -> List[str]:
pending_native = getattr(self, "_pending_native_image_paths_by_session", None)
if not pending_native:

View file

@ -12,7 +12,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from gateway.config import GatewayConfig, Platform, PlatformConfig
from gateway.platforms.base import MessageEvent
from gateway.platforms.base import MessageEvent, MessageType
from gateway.session import SessionEntry, SessionSource, build_session_key
@ -30,6 +30,18 @@ def _make_event(text: str) -> MessageEvent:
return MessageEvent(text=text, source=_make_source(), message_id="m1")
def _make_voice_event(text: str = "voice_message_1.ogg") -> MessageEvent:
source = _make_source()
return MessageEvent(
text=text,
message_type=MessageType.VOICE,
source=source,
message_id="m1",
media_urls=["/tmp/voice_message_1.ogg"],
media_types=["audio/ogg"],
)
def _make_runner():
from gateway.run import GatewayRunner
@ -200,6 +212,67 @@ async def test_underscored_alias_for_hyphenated_builtin_not_flagged(monkeypatch)
assert "Unknown command" not in result
@pytest.mark.asyncio
@pytest.mark.parametrize("event_text", ["voice_message_1.ogg", ""])
async def test_pending_clarify_voice_reply_uses_transcript_and_choice_coercion(
monkeypatch,
event_text,
):
"""Filename-bearing and captionless voice replies resolve from raw STT text."""
import gateway.run as gateway_run
from tools import clarify_gateway
runner = _make_runner()
session_key = build_session_key(_make_source())
clarify_id = f"clarify-voice-{event_text or 'empty'}"
clarify_gateway.register(
clarify_id,
session_key,
"Pick one",
["first choice", "second choice"],
)
runner.hooks.emit_collect = AsyncMock(return_value=[])
runner._transcribe_pending_audio_event_once = AsyncMock(
return_value=('"2"', ["2"]),
)
monkeypatch.setattr(
gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"}
)
result = await runner._handle_message(_make_voice_event(event_text))
assert result == ""
runner._transcribe_pending_audio_event_once.assert_awaited_once()
assert clarify_gateway.wait_for_response(clarify_id, timeout=0.1) == "second choice"
@pytest.mark.asyncio
async def test_failed_clarify_voice_transcription_does_not_resolve_marker(monkeypatch):
"""An STT status marker is not a user answer; the clarify stays pending."""
import gateway.run as gateway_run
from tools import clarify_gateway
runner = _make_runner()
event = _make_voice_event("")
session_key = build_session_key(event.source)
clarify_id = "clarify-voice-stt-failure"
clarify_gateway.register(clarify_id, session_key, "Say anything", None)
runner._transcribe_pending_audio_event_once = AsyncMock(
return_value=("[voice message could not be transcribed]", []),
)
monkeypatch.setattr(
gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"}
)
try:
assert await runner._handle_message(event) == ""
runner._transcribe_pending_audio_event_once.assert_awaited_once()
assert clarify_gateway.has_pending(session_key) is True
finally:
clarify_gateway.clear_session(session_key)
# ------------------------------------------------------------------
# command:<name> decision hook — deny / handled / rewrite
# ------------------------------------------------------------------