fix: handle farewells and stabilize voice responses
This commit is contained in:
+5
-5
@@ -9,7 +9,7 @@ LLM_PROVIDER=openai
|
|||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
OPENAI_BASE_URL=
|
OPENAI_BASE_URL=
|
||||||
OPENAI_LLM_MODEL=gpt-4o-mini
|
OPENAI_LLM_MODEL=gpt-4o-mini
|
||||||
OPENAI_LLM_SYSTEM_PROMPT=You are Ainur, a concise Russian-speaking voice agent for the DigiOps contact center. Always answer in Russian, naturally and briefly for voice playback. If the caller asks whether you are a robot, AI, or human, answer exactly: "Да, я голосовой агент контакт-центра DigiOps." Read phone numbers digit by digit. Prefer clear stress-friendly wording. Do not use markdown, URLs, or tables.
|
OPENAI_LLM_SYSTEM_PROMPT=You are Ainur, a concise Russian-speaking voice agent for the DigiOps contact center. Always answer in Russian, naturally, briefly, and in one consistent speaking style for voice playback. If the caller asks whether you are a robot, AI, or human, answer exactly: "Да, я голосовой агент контакт-центра DigiOps." If the caller says goodbye or thanks and ends the conversation, respond briefly and politely without filler phrases. Read phone numbers digit by digit. Prefer clear stress-friendly wording. Do not use markdown, URLs, or tables.
|
||||||
OPENAI_TIMEOUT_SECONDS=30
|
OPENAI_TIMEOUT_SECONDS=30
|
||||||
OPENAI_STT_MODEL=whisper-1
|
OPENAI_STT_MODEL=whisper-1
|
||||||
OPENAI_STT_LANGUAGE=ru
|
OPENAI_STT_LANGUAGE=ru
|
||||||
@@ -26,7 +26,7 @@ OLLAMA_LLM_NUM_CTX=1024
|
|||||||
|
|
||||||
STT_PROVIDER=openai
|
STT_PROVIDER=openai
|
||||||
STT_FALLBACK_PROVIDER=elevenlabs
|
STT_FALLBACK_PROVIDER=elevenlabs
|
||||||
STT_PROMPT=Это разговор на русском языке, но могут встречаться казахские имена: Айбын, Магжан, Бауыржан, Асель.
|
STT_PROMPT=Это русская речь. Точно распознавай имена, фамилии, слова благодарности и прощания. Возможны имена: Айнур, Айгерим, Алия, Данияр, Азамат, Мадина, Нурсултан. Номера телефонов записывай цифрами.
|
||||||
TTS_PROVIDER=elevenlabs
|
TTS_PROVIDER=elevenlabs
|
||||||
|
|
||||||
ENABLE_AUDIO_DUMP=false
|
ENABLE_AUDIO_DUMP=false
|
||||||
@@ -54,12 +54,12 @@ YANDEX_STT_TOPIC=general
|
|||||||
|
|
||||||
VAD_THRESHOLD=0.5
|
VAD_THRESHOLD=0.5
|
||||||
VAD_NEGATIVE_THRESHOLD=
|
VAD_NEGATIVE_THRESHOLD=
|
||||||
VAD_SILENCE_TIMEOUT_MS=350
|
VAD_SILENCE_TIMEOUT_MS=420
|
||||||
VAD_SPEECH_PAD_MS=64
|
VAD_SPEECH_PAD_MS=120
|
||||||
VAD_MIN_SPEECH_DURATION_MS=0
|
VAD_MIN_SPEECH_DURATION_MS=0
|
||||||
VAD_USE_ONNX=false
|
VAD_USE_ONNX=false
|
||||||
REALTIME_VOICE_INITIAL_GREETING_TEXT=Здравствуйте! Меня зовут Айнур, я эй-ай-ассистент компании ДиджиОпс. Как я могу к вам обращаться?
|
REALTIME_VOICE_INITIAL_GREETING_TEXT=Здравствуйте! Меня зовут Айнур, я эй-ай-ассистент компании ДиджиОпс. Как я могу к вам обращаться?
|
||||||
SEMANTIC_ENDPOINTING_HOLD_MS=600
|
SEMANTIC_ENDPOINTING_HOLD_MS=600
|
||||||
REALTIME_VOICE_FILLER_DELAY_MS=250
|
REALTIME_VOICE_FILLER_DELAY_MS=200
|
||||||
REALTIME_VOICE_TTS_CHUNK_SOFT_MIN_CHARS=10
|
REALTIME_VOICE_TTS_CHUNK_SOFT_MIN_CHARS=10
|
||||||
REALTIME_VOICE_TTS_CHUNK_SOFT_MIN_WORDS=2
|
REALTIME_VOICE_TTS_CHUNK_SOFT_MIN_WORDS=2
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ LOGGER = logging.getLogger("uvicorn.error")
|
|||||||
DEFAULT_FILLER_TEXTS: dict[str, tuple[str, ...]] = {
|
DEFAULT_FILLER_TEXTS: dict[str, tuple[str, ...]] = {
|
||||||
"serper": (
|
"serper": (
|
||||||
"Сейчас уточню.",
|
"Сейчас уточню.",
|
||||||
"Минуту, пожалуйста.",
|
"Проверяю информацию.",
|
||||||
),
|
),
|
||||||
"generic": (
|
"generic": (
|
||||||
"Секунду.",
|
"Сейчас отвечу.",
|
||||||
"Минуту.",
|
"Уточняю.",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,49 @@ SEMANTIC_ENDPOINTING_HOLD_MS = _semantic_hold_ms()
|
|||||||
FILLER_AUDIO_DELAY_MS = _filler_delay_ms()
|
FILLER_AUDIO_DELAY_MS = _filler_delay_ms()
|
||||||
TTS_CHUNK_SOFT_MIN_CHARS = _tts_chunk_soft_min_chars()
|
TTS_CHUNK_SOFT_MIN_CHARS = _tts_chunk_soft_min_chars()
|
||||||
TTS_CHUNK_SOFT_MIN_WORDS = _tts_chunk_soft_min_words()
|
TTS_CHUNK_SOFT_MIN_WORDS = _tts_chunk_soft_min_words()
|
||||||
|
_FAREWELL_MARKERS: frozenset[str] = frozenset({
|
||||||
|
"спасибо",
|
||||||
|
"благодарю",
|
||||||
|
"до свидания",
|
||||||
|
"всего доброго",
|
||||||
|
"пока",
|
||||||
|
"до встречи",
|
||||||
|
"хорошего дня",
|
||||||
|
"goodbye",
|
||||||
|
"bye",
|
||||||
|
})
|
||||||
|
_FAREWELL_QUERY_MARKERS: frozenset[str] = frozenset({
|
||||||
|
"вопрос",
|
||||||
|
"подскаж",
|
||||||
|
"скажи",
|
||||||
|
"почему",
|
||||||
|
"как",
|
||||||
|
"что",
|
||||||
|
"где",
|
||||||
|
"когда",
|
||||||
|
"можно",
|
||||||
|
"нужно",
|
||||||
|
"хочу",
|
||||||
|
"мне",
|
||||||
|
})
|
||||||
|
FAREWELL_RESPONSE_TEXT = "Спасибо за обращение в DigiOps. Всего доброго, до свидания!"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_farewell_transcript(text: str) -> bool:
|
||||||
|
normalized = _voice_text_key(text)
|
||||||
|
return any(marker in normalized for marker in _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):
|
||||||
|
return False
|
||||||
|
if any(marker in normalized for marker in _FAREWELL_QUERY_MARKERS):
|
||||||
|
return False
|
||||||
|
word_count = len(re.findall(r"[^\W\d_]+(?:[-'][^\W\d_]+)*", normalized, flags=re.UNICODE))
|
||||||
|
return word_count <= 10
|
||||||
|
|
||||||
|
|
||||||
SEMANTIC_CONTINUATION_TOKENS = {
|
SEMANTIC_CONTINUATION_TOKENS = {
|
||||||
"а",
|
"а",
|
||||||
"в",
|
"в",
|
||||||
@@ -927,6 +970,20 @@ class CallSession:
|
|||||||
await self._assistant_task
|
await self._assistant_task
|
||||||
await self.transport.close()
|
await self.transport.close()
|
||||||
|
|
||||||
|
async def _close_after_farewell(self) -> None:
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
self._closed = True
|
||||||
|
LOGGER.info(
|
||||||
|
"realtime session %s closing transport after farewell: state=%s epoch=%s conversation_entries=%s",
|
||||||
|
self.session_id,
|
||||||
|
self.state.value,
|
||||||
|
self.generation_epoch,
|
||||||
|
len(self._conversation),
|
||||||
|
)
|
||||||
|
await self._cancel_live_stt_stream()
|
||||||
|
await self.transport.close()
|
||||||
|
|
||||||
def _start_assistant_turn(
|
def _start_assistant_turn(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -1047,6 +1104,26 @@ class CallSession:
|
|||||||
)
|
)
|
||||||
self._set_state(SessionState.LISTENING, reason="name collection completed")
|
self._set_state(SessionState.LISTENING, reason="name collection completed")
|
||||||
return
|
return
|
||||||
|
if _is_terminal_farewell(transcript):
|
||||||
|
LOGGER.info(
|
||||||
|
"realtime session %s farewell response prepared: epoch=%s text=%r",
|
||||||
|
self.session_id,
|
||||||
|
epoch,
|
||||||
|
_preview_text(FAREWELL_RESPONSE_TEXT),
|
||||||
|
)
|
||||||
|
await self._play_prepared_response(
|
||||||
|
epoch=epoch,
|
||||||
|
response_text=FAREWELL_RESPONSE_TEXT,
|
||||||
|
actually_spoken_chunks=actually_spoken_chunks,
|
||||||
|
)
|
||||||
|
self._ensure_generation(epoch)
|
||||||
|
self._commit_assistant_context(
|
||||||
|
llm_generated_text=FAREWELL_RESPONSE_TEXT,
|
||||||
|
actually_spoken_chunks=actually_spoken_chunks,
|
||||||
|
interrupted=False,
|
||||||
|
)
|
||||||
|
await self._close_after_farewell()
|
||||||
|
return
|
||||||
sentence_queue = asyncio.Queue()
|
sentence_queue = asyncio.Queue()
|
||||||
self._sentence_queue = sentence_queue
|
self._sentence_queue = sentence_queue
|
||||||
playback_task = asyncio.create_task(
|
playback_task = asyncio.create_task(
|
||||||
@@ -1066,6 +1143,7 @@ class CallSession:
|
|||||||
epoch=epoch,
|
epoch=epoch,
|
||||||
tool_name=None,
|
tool_name=None,
|
||||||
started_monotonic=llm_started_monotonic,
|
started_monotonic=llm_started_monotonic,
|
||||||
|
suppress=_is_farewell_transcript(transcript),
|
||||||
),
|
),
|
||||||
name=f"{self.session_id}-filler-delay-{epoch}",
|
name=f"{self.session_id}-filler-delay-{epoch}",
|
||||||
)
|
)
|
||||||
@@ -1618,7 +1696,10 @@ class CallSession:
|
|||||||
epoch: int,
|
epoch: int,
|
||||||
tool_name: str | None,
|
tool_name: str | None,
|
||||||
started_monotonic: float,
|
started_monotonic: float,
|
||||||
|
suppress: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if suppress:
|
||||||
|
return
|
||||||
if FILLER_AUDIO_DELAY_MS <= 0:
|
if FILLER_AUDIO_DELAY_MS <= 0:
|
||||||
self._start_filler_audio(
|
self._start_filler_audio(
|
||||||
epoch=epoch,
|
epoch=epoch,
|
||||||
|
|||||||
Reference in New Issue
Block a user