feat: enhance AudioSocketMediaRuntime to skip filler acks for closing intents and throttle repeated filler acks
deploy / deploy (push) Successful in 30s

This commit is contained in:
2026-08-30 11:30:55 +05:00
parent a013ac95e8
commit 9e4c47eddd
2 changed files with 336 additions and 3 deletions
@@ -217,6 +217,10 @@ class AudioSocketMediaRuntime:
self._immediate_ack_min_ms = 700
self._v2_ack_post_gap_seconds = 0.10
self._v1_ack_wait_seconds = 0.6
# Skip a would-be filler if the previous one finished too recently, so rapid
# back-and-forth turns (e.g. a caller spelling out a phone number field by
# field) don't get a filler read before every single fragment.
self._ack_min_repeat_gap_seconds = 2.5
self._partial_poll_interval_seconds = 0.20
# Small startup cushion for streamed TTS playback: absorb ElevenLabs
# network delivery jitter before we start pacing frames out to the
@@ -314,10 +318,32 @@ class AudioSocketMediaRuntime:
return True
return normalized in cls._FINAL_LOW_SIGNAL_PHRASES
_CLOSING_INTENT_PHRASES = (
"до свидания",
"всего доброго",
"хорошего дня",
"хорошего вечера",
"прощайте",
"созвонимся",
"это все спасибо",
"это всё спасибо",
"у меня все спасибо",
"у меня всё спасибо",
"больше вопросов нет",
"вопросов больше нет",
"спасибо за помощь",
"спасибо большое до свидания",
"сау болыңыз",
"келесіге дейін",
"рахмет көп",
)
def _detect_early_intent(self, text: str) -> str:
normalized = self._normalize_intent_text(text)
if not normalized:
return "unknown"
if any(token in normalized for token in self._CLOSING_INTENT_PHRASES):
return "closing"
if any(token in normalized for token in ("оператор", "оператором", "человеком", "менеджер", "сотрудник")):
return "operator_request"
if any(token in normalized for token in ("график", "распис", "время работы", "work schedule", "жұмыс")):
@@ -347,6 +373,8 @@ class AudioSocketMediaRuntime:
@staticmethod
def _ack_kind_for_intent(intent: str) -> str:
if intent == "closing":
return "closing"
if intent == "operator_request":
return "handoff"
if intent in {"schedule", "address", "price", "status", "problem"}:
@@ -406,8 +434,18 @@ class AudioSocketMediaRuntime:
return True
return normalized_intent != "unknown"
def _should_emit_blind_ack(self, actor: MediaActor, pcm_bytes: bytes) -> bool:
def _should_emit_blind_ack(self, actor: MediaActor, pcm_bytes: bytes, partial_transcript: str) -> bool:
"""Duration-only fallback for when no usable partial transcript exists yet.
Must defer to the transcript when one *is* available: otherwise a caller
who already said a recognized filler-answer ("да"/"нет"/"хорошо") still
gets a blind ack just because the audio happened to cross the length
threshold, even though `_should_emit_partial_ack` correctly said no.
"""
del actor
transcript_text = str(partial_transcript or "").strip()
if transcript_text and self._is_low_signal_partial_transcript(transcript_text):
return False
return len(pcm_bytes) >= self._immediate_ack_min_bytes
@staticmethod
@@ -1116,10 +1154,22 @@ class AudioSocketMediaRuntime:
metadata: dict[str, Any],
ack_source: str,
ack_kind: str | None = None,
intent: str | None = None,
) -> None:
if actor.closed or actor.early_ack_started:
return
ack_kind = ack_kind or self._ack_kind_for_intent(actor.partial_intent or "unknown")
effective_intent = str(
intent or actor.stable_partial_intent or actor.partial_intent or "unknown"
).strip() or "unknown"
if effective_intent == "closing":
# The caller is wrapping up; a "thinking" filler right before the
# closing reply reads as robotic, so skip it and go straight to the reply.
return
if actor.last_ack_completed_monotonic and (
time.monotonic() - actor.last_ack_completed_monotonic
) < self._ack_min_repeat_gap_seconds:
return
ack_kind = ack_kind or self._ack_kind_for_intent(effective_intent)
ack_text, style_hints, ack_variant = self._select_ack_payload(
actor,
language=language,
@@ -1620,8 +1670,9 @@ class AudioSocketMediaRuntime:
language=actor.registration.language,
metadata=base_metadata,
ack_source="streaming_partial" if actor.asr_streaming_enabled else "precomputed_partial_asr",
intent=partial_intent,
)
elif self._should_emit_blind_ack(actor, pcm_bytes):
elif self._should_emit_blind_ack(actor, pcm_bytes, partial_transcript):
await self._emit_early_ack(
actor,
language=actor.registration.language,