Fix TTS time phrases and STT cleanup

This commit is contained in:
2026-03-15 16:22:00 +03:00
parent cb54a9ee75
commit 6add70fcd2
4 changed files with 146 additions and 2 deletions

View File

@@ -234,6 +234,33 @@ class SpeechRecognizer:
return True
def _run_blocking_cleanup_sync(self, func, timeout_seconds: float, label: str) -> bool:
"""Sync-версия _run_blocking_cleanup() для use-case в listen()."""
done_event = threading.Event()
error_holder = {}
def runner():
try:
func()
except Exception as exc:
error_holder["error"] = exc
finally:
done_event.set()
thread = threading.Thread(target=runner, daemon=True, name=label)
thread.start()
done_event.wait(timeout=max(0.0, float(timeout_seconds)))
if not done_event.is_set():
print(f"⚠️ {label} timed out; continuing cleanup.")
return False
error = error_holder.get("error")
if error is not None:
print(f"⚠️ {label} failed: {error}")
return False
return True
async def _process_audio(
self, dg_connection, timeout_seconds, detection_timeout, fast_stop
):
@@ -334,6 +361,7 @@ class SpeechRecognizer:
# --- Задача отправки аудио с буферизацией ---
sender_stop_event = threading.Event()
stream_holder = {"stream": None}
def request_stop():
stop_event.set()
@@ -346,6 +374,7 @@ class SpeechRecognizer:
try:
stream, stream_sample_rate = self._open_stream_for_session()
stream_holder["stream"] = stream
options = LiveOptions(
model="nova-2", # Самая быстрая и точная модель
language=self.current_lang,
@@ -465,6 +494,7 @@ class SpeechRecognizer:
with contextlib.suppress(Exception):
if stream:
stream.close()
stream_holder["stream"] = None
print(f"\n🛑 Stream stopped. Chunks sent: {chunks_sent}")
sender_thread = threading.Thread(
@@ -544,23 +574,56 @@ class SpeechRecognizer:
sender_thread,
timeout_seconds=max(SENDER_STOP_WAIT_SECONDS, SENDER_FORCE_RELEASE_WAIT_SECONDS),
)
cleanup_unhealthy = False
if not sender_stopped:
print("⚠️ Audio sender shutdown timed out; continuing cleanup.")
cleanup_unhealthy = True
def force_close_stream():
stream = stream_holder.get("stream")
if not stream:
return
with contextlib.suppress(Exception):
if stream.is_active():
stream.stop_stream()
with contextlib.suppress(Exception):
stream.close()
stream_holder["stream"] = None
await self._run_blocking_cleanup(
force_close_stream,
timeout_seconds=SENDER_FORCE_RELEASE_WAIT_SECONDS,
label="STT audio stream force close",
)
# Дадим шанс потоку выйти после принудительного закрытия.
sender_stopped = await self._wait_for_thread(sender_thread, timeout_seconds=0.6)
if not sender_stopped:
cleanup_unhealthy = True
# Небольшая пауза, чтобы получить последние transcript-события перед finish().
await asyncio.sleep(DEEPGRAM_FINALIZATION_GRACE_SECONDS)
# Завершаем соединение и ждем последние результаты
await self._run_blocking_cleanup(
finish_ok = await self._run_blocking_cleanup(
dg_connection.finish,
timeout_seconds=DEEPGRAM_FINISH_TIMEOUT_SECONDS,
label="Deepgram finish",
)
if not finish_ok:
cleanup_unhealthy = True
final_text = self.transcript.strip()
if not final_text:
final_text = latest_interim.strip()
self.transcript = final_text
if cleanup_unhealthy:
# Если текст уже получен, не теряем команду пользователя.
# Но сбрасываем клиента, чтобы следующая STT-сессия стартовала на чистом соединении.
self.dg_client = None
if final_text:
return final_text
raise RuntimeError("Deepgram session cleanup timed out")
return final_text
def listen(
@@ -622,10 +685,20 @@ class SpeechRecognizer:
# Закрываем соединение, если оно было создано
if dg_connection:
try:
dg_connection.finish()
self._run_blocking_cleanup_sync(
dg_connection.finish,
timeout_seconds=DEEPGRAM_FINISH_TIMEOUT_SECONDS,
label="Deepgram finish (error cleanup)",
)
except:
pass # Игнорируем ошибки при завершении
# Принудительно сбрасываем клиента, чтобы след. попытка не унаследовала
# подвисшее соединение SDK.
self.dg_client = None
with contextlib.suppress(Exception):
self.initialize()
if attempt < 2: # Не ждем после последней попытки
print(f"⚠️ Не удалось подключиться к Deepgram, попытка {attempt + 1}/3, повторяю...")
time.sleep(1) # Уменьшаем задержку между попытками

View File

@@ -147,6 +147,73 @@ def numbers_to_words(text: str) -> str:
preps_list = "|".join(map(re.escape, PREPOSITION_CASES.keys()))
# Время вида "в 7:00" / "во 7:00" / "к 7:05" / "07:00" -> человеческая русская форма.
# Важно: "в семь" (не "в семи"), "к семи" (дательный).
def _minute_words(minute_val: int) -> str:
if minute_val == 0:
return "ровно"
if minute_val < 10:
return "ноль " + convert_number(
str(minute_val), context_type="cardinal", case="nominative", gender="m"
)
return convert_number(str(minute_val), context_type="cardinal", case="nominative", gender="m")
def replace_time_match(match):
prep = match.group(1) or ""
hour_str = match.group(2)
minute_str = match.group(3)
try:
hour_val = int(hour_str)
minute_val = int(minute_str)
except Exception:
return match.group(0)
if not (0 <= hour_val <= 23 and 0 <= minute_val <= 59):
return match.group(0)
prep_clean = prep.strip().lower()
if prep_clean in {"в", "во"}:
hour_case = "accusative"
elif prep_clean in {"к", "ко"}:
hour_case = "dative"
else:
hour_case = "nominative"
hour_words = convert_number(str(hour_val), context_type="cardinal", case=hour_case, gender="m")
minute_words = _minute_words(minute_val)
prefix = f"{prep} " if prep else ""
return f"{prefix}{hour_words} {minute_words}"
def replace_time_no_prep_match(match):
hour_str = match.group(1)
minute_str = match.group(2)
try:
hour_val = int(hour_str)
minute_val = int(minute_str)
except Exception:
return match.group(0)
if not (0 <= hour_val <= 23 and 0 <= minute_val <= 59):
return match.group(0)
hour_words = convert_number(str(hour_val), context_type="cardinal", case="nominative", gender="m")
minute_words = _minute_words(minute_val)
return f"{hour_words} {minute_words}"
text = re.sub(
r"(?i)\b(в|во|к|ко)\s+(\d{1,2})\s*:\s*(\d{2})\b",
replace_time_match,
text,
)
text = re.sub(
r"\b(\d{1,2})\s*:\s*(\d{2})\b",
replace_time_no_prep_match,
text,
)
# Года с суффиксом
def replace_year_suffix_match(match):
prep = match.group(1)

View File

@@ -335,6 +335,7 @@ def main():
if not user_text:
# Молчание — возвращаемся к ожиданию
print("user was not talking")
skip_wakeword = False
continue