.
This commit is contained in:
+106
-9
@@ -15,6 +15,7 @@ from collections.abc import Iterable
|
||||
from enum import Enum
|
||||
|
||||
from realtime_voice_service.core.filler_audio import FillerAudioLibrary
|
||||
from realtime_voice_service.core.kazakh_names import TOP_KAZAKH_NAMES
|
||||
from realtime_voice_service.core.vad import BaseVAD, SileroVADDetector
|
||||
from realtime_voice_service.providers.base import BaseLLM, BaseSTT, BaseSTTStream, BaseTTS, MockLLM, MockSTT, MockTTS
|
||||
from realtime_voice_service.transports.base import BaseMediaTransport
|
||||
@@ -53,7 +54,7 @@ FILLER_AUDIO_DELAY_MS = _filler_delay_ms()
|
||||
TTS_CHUNK_SOFT_MIN_CHARS = _tts_chunk_soft_min_chars()
|
||||
TTS_CHUNK_SOFT_MIN_WORDS = _tts_chunk_soft_min_words()
|
||||
|
||||
_FAREWELL_MARKERS: frozenset[str] = frozenset({
|
||||
_COURTESY_OR_FAREWELL_MARKERS: frozenset[str] = frozenset({
|
||||
"спасибо",
|
||||
"благодарю",
|
||||
"до свидания",
|
||||
@@ -77,6 +78,48 @@ _FAREWELL_MARKERS: frozenset[str] = frozenset({
|
||||
"bye",
|
||||
})
|
||||
|
||||
_TERMINAL_DIRECT_FAREWELL_MARKERS: frozenset[str] = frozenset({
|
||||
"до свидания",
|
||||
"всего доброго",
|
||||
"пока",
|
||||
"до встречи",
|
||||
"хорошего дня",
|
||||
"goodbye",
|
||||
"bye",
|
||||
})
|
||||
|
||||
_TERMINAL_CLOSE_REQUEST_MARKERS: frozenset[str] = frozenset({
|
||||
"можно завершить",
|
||||
"можем завершить",
|
||||
"можете завершать",
|
||||
"давайте завершим",
|
||||
"давайте закончим",
|
||||
"завершить разговор",
|
||||
"закончить разговор",
|
||||
"заканчиваем разговор",
|
||||
"завершайте разговор",
|
||||
"кладу трубку",
|
||||
"положу трубку",
|
||||
})
|
||||
|
||||
_TERMINAL_NO_MORE_HELP_MARKERS: frozenset[str] = frozenset({
|
||||
"ничего не нужно",
|
||||
"ничего не надо",
|
||||
"мне ничего не нужно",
|
||||
"мне ничего не надо",
|
||||
"больше ничего",
|
||||
"больше ничего не нужно",
|
||||
"больше ничего не надо",
|
||||
"ничего больше не нужно",
|
||||
"ничего больше не надо",
|
||||
"это все",
|
||||
"это всё",
|
||||
"на этом все",
|
||||
"на этом всё",
|
||||
"вопросов нет",
|
||||
"больше вопросов нет",
|
||||
})
|
||||
|
||||
_FAREWELL_QUERY_MARKERS: frozenset[str] = frozenset({
|
||||
"вопрос",
|
||||
"подскаж",
|
||||
@@ -90,7 +133,6 @@ _FAREWELL_QUERY_MARKERS: frozenset[str] = frozenset({
|
||||
"нужно ли",
|
||||
"нужно еще",
|
||||
"нужно ещё",
|
||||
"хочу",
|
||||
"еще",
|
||||
"ещё",
|
||||
"теперь",
|
||||
@@ -107,22 +149,63 @@ _FAREWELL_QUERY_MARKERS: frozenset[str] = frozenset({
|
||||
"нужна",
|
||||
"нужны",
|
||||
})
|
||||
|
||||
_FAREWELL_HARD_QUERY_MARKERS: frozenset[str] = frozenset(
|
||||
marker for marker in _FAREWELL_QUERY_MARKERS if marker not in {"вопрос", "можно"}
|
||||
)
|
||||
FAREWELL_RESPONSE_TEXT = "Спасибо за обращение в DigiOps. Всего доброго, до свидания!"
|
||||
|
||||
|
||||
def _contains_voice_phrase(normalized_text: str, phrase: str) -> bool:
|
||||
phrase_parts = [re.escape(part) for part in phrase.split()]
|
||||
if not phrase_parts:
|
||||
return False
|
||||
phrase_pattern = r"\s+".join(phrase_parts)
|
||||
return re.search(
|
||||
rf"(?<![^\W\d_]){phrase_pattern}(?![^\W\d_])",
|
||||
normalized_text,
|
||||
flags=re.UNICODE,
|
||||
) is not None
|
||||
|
||||
|
||||
def _ends_with_voice_phrase(normalized_text: str, phrase: str) -> bool:
|
||||
phrase_parts = [re.escape(part) for part in phrase.split()]
|
||||
if not phrase_parts:
|
||||
return False
|
||||
phrase_pattern = r"\s+".join(phrase_parts)
|
||||
return re.search(
|
||||
rf"(?<![^\W\d_]){phrase_pattern}(?:\s+(?:спасибо|благодарю))?$",
|
||||
normalized_text,
|
||||
flags=re.UNICODE,
|
||||
) is not None
|
||||
|
||||
|
||||
def _is_farewell_transcript(text: str) -> bool:
|
||||
normalized = _voice_text_key(text)
|
||||
return any(marker in normalized for marker in _FAREWELL_MARKERS)
|
||||
return any(_contains_voice_phrase(normalized, marker) for marker in _COURTESY_OR_FAREWELL_MARKERS)
|
||||
|
||||
|
||||
def _is_terminal_farewell(text: str) -> bool:
|
||||
normalized = _voice_text_key(text)
|
||||
if not any(marker in normalized for marker in _FAREWELL_MARKERS):
|
||||
has_direct_farewell = any(
|
||||
_contains_voice_phrase(normalized, marker)
|
||||
for marker in _TERMINAL_DIRECT_FAREWELL_MARKERS
|
||||
)
|
||||
has_close_request = any(
|
||||
_contains_voice_phrase(normalized, marker)
|
||||
for marker in _TERMINAL_CLOSE_REQUEST_MARKERS
|
||||
)
|
||||
has_no_more_help = any(
|
||||
_ends_with_voice_phrase(normalized, marker)
|
||||
for marker in _TERMINAL_NO_MORE_HELP_MARKERS
|
||||
)
|
||||
if not (has_direct_farewell or has_close_request or has_no_more_help):
|
||||
return False
|
||||
if any(marker in normalized for marker in _FAREWELL_QUERY_MARKERS):
|
||||
query_markers = _FAREWELL_QUERY_MARKERS if has_direct_farewell else _FAREWELL_HARD_QUERY_MARKERS
|
||||
if any(marker in normalized for marker in query_markers):
|
||||
return False
|
||||
word_count = len(re.findall(r"[^\W\d_]+(?:[-'][^\W\d_]+)*", normalized, flags=re.UNICODE))
|
||||
return word_count <= 10
|
||||
return word_count <= 16
|
||||
|
||||
|
||||
SEMANTIC_CONTINUATION_TOKENS = {
|
||||
@@ -1405,7 +1488,12 @@ class CallSession:
|
||||
len(audio_bytes),
|
||||
_audio_duration_ms(audio_bytes, sample_rate_hz=self.transport.sample_rate_hz),
|
||||
)
|
||||
pending_transcript = (await self._stt.transcribe(audio_bytes)).strip()
|
||||
pending_transcript = (
|
||||
await self._stt.transcribe(
|
||||
audio_bytes,
|
||||
keyterms=self._name_capture_keyterms() if self._awaiting_customer_name else None,
|
||||
)
|
||||
).strip()
|
||||
except Exception:
|
||||
LOGGER.exception("realtime session %s failed to transcribe pending unanswered audio", self.session_id)
|
||||
continue
|
||||
@@ -1421,6 +1509,9 @@ class CallSession:
|
||||
)
|
||||
return pending_texts
|
||||
|
||||
def _name_capture_keyterms(self) -> tuple[str, ...]:
|
||||
return TOP_KAZAKH_NAMES
|
||||
|
||||
def _build_name_collection_response(self, transcript: str) -> str | None:
|
||||
if not self._awaiting_customer_name:
|
||||
return None
|
||||
@@ -1880,7 +1971,10 @@ class CallSession:
|
||||
self._reset_semantic_endpointing()
|
||||
try:
|
||||
LOGGER.info("realtime session %s live STT stream start requested", self.session_id)
|
||||
live_stt_stream = await self._stt.start_stream(partial_callback=self._handle_partial_transcript)
|
||||
live_stt_stream = await self._stt.start_stream(
|
||||
partial_callback=self._handle_partial_transcript,
|
||||
keyterms=self._name_capture_keyterms() if self._awaiting_customer_name else None,
|
||||
)
|
||||
except Exception:
|
||||
LOGGER.exception("realtime session %s failed to start live STT stream", self.session_id)
|
||||
return
|
||||
@@ -1953,7 +2047,10 @@ class CallSession:
|
||||
len(utterance_audio),
|
||||
_audio_duration_ms(utterance_audio, sample_rate_hz=self.transport.sample_rate_hz),
|
||||
)
|
||||
return await self._stt.transcribe(utterance_audio)
|
||||
return await self._stt.transcribe(
|
||||
utterance_audio,
|
||||
keyterms=self._name_capture_keyterms() if self._awaiting_customer_name else None,
|
||||
)
|
||||
|
||||
def _maybe_dump_audio(self, audio_bytes: bytes) -> None:
|
||||
if str(os.getenv("ENABLE_AUDIO_DUMP", "")).strip().lower() not in {"1", "true", "yes", "on"}:
|
||||
|
||||
Reference in New Issue
Block a user