Fix TTS time phrases and STT cleanup
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -46,6 +46,9 @@ vosk-model-*/
|
|||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
|
# Runtime state
|
||||||
|
data/music_state.json
|
||||||
|
|
||||||
|
|
||||||
.beads
|
.beads
|
||||||
.gitattributes
|
.gitattributes
|
||||||
|
|||||||
@@ -234,6 +234,33 @@ class SpeechRecognizer:
|
|||||||
|
|
||||||
return True
|
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(
|
async def _process_audio(
|
||||||
self, dg_connection, timeout_seconds, detection_timeout, fast_stop
|
self, dg_connection, timeout_seconds, detection_timeout, fast_stop
|
||||||
):
|
):
|
||||||
@@ -334,6 +361,7 @@ class SpeechRecognizer:
|
|||||||
|
|
||||||
# --- Задача отправки аудио с буферизацией ---
|
# --- Задача отправки аудио с буферизацией ---
|
||||||
sender_stop_event = threading.Event()
|
sender_stop_event = threading.Event()
|
||||||
|
stream_holder = {"stream": None}
|
||||||
|
|
||||||
def request_stop():
|
def request_stop():
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
@@ -346,6 +374,7 @@ class SpeechRecognizer:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
stream, stream_sample_rate = self._open_stream_for_session()
|
stream, stream_sample_rate = self._open_stream_for_session()
|
||||||
|
stream_holder["stream"] = stream
|
||||||
options = LiveOptions(
|
options = LiveOptions(
|
||||||
model="nova-2", # Самая быстрая и точная модель
|
model="nova-2", # Самая быстрая и точная модель
|
||||||
language=self.current_lang,
|
language=self.current_lang,
|
||||||
@@ -465,6 +494,7 @@ class SpeechRecognizer:
|
|||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
if stream:
|
if stream:
|
||||||
stream.close()
|
stream.close()
|
||||||
|
stream_holder["stream"] = None
|
||||||
print(f"\n🛑 Stream stopped. Chunks sent: {chunks_sent}")
|
print(f"\n🛑 Stream stopped. Chunks sent: {chunks_sent}")
|
||||||
|
|
||||||
sender_thread = threading.Thread(
|
sender_thread = threading.Thread(
|
||||||
@@ -544,23 +574,56 @@ class SpeechRecognizer:
|
|||||||
sender_thread,
|
sender_thread,
|
||||||
timeout_seconds=max(SENDER_STOP_WAIT_SECONDS, SENDER_FORCE_RELEASE_WAIT_SECONDS),
|
timeout_seconds=max(SENDER_STOP_WAIT_SECONDS, SENDER_FORCE_RELEASE_WAIT_SECONDS),
|
||||||
)
|
)
|
||||||
|
cleanup_unhealthy = False
|
||||||
if not sender_stopped:
|
if not sender_stopped:
|
||||||
print("⚠️ Audio sender shutdown timed out; continuing cleanup.")
|
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().
|
# Небольшая пауза, чтобы получить последние transcript-события перед finish().
|
||||||
await asyncio.sleep(DEEPGRAM_FINALIZATION_GRACE_SECONDS)
|
await asyncio.sleep(DEEPGRAM_FINALIZATION_GRACE_SECONDS)
|
||||||
|
|
||||||
# Завершаем соединение и ждем последние результаты
|
# Завершаем соединение и ждем последние результаты
|
||||||
await self._run_blocking_cleanup(
|
finish_ok = await self._run_blocking_cleanup(
|
||||||
dg_connection.finish,
|
dg_connection.finish,
|
||||||
timeout_seconds=DEEPGRAM_FINISH_TIMEOUT_SECONDS,
|
timeout_seconds=DEEPGRAM_FINISH_TIMEOUT_SECONDS,
|
||||||
label="Deepgram finish",
|
label="Deepgram finish",
|
||||||
)
|
)
|
||||||
|
if not finish_ok:
|
||||||
|
cleanup_unhealthy = True
|
||||||
|
|
||||||
final_text = self.transcript.strip()
|
final_text = self.transcript.strip()
|
||||||
if not final_text:
|
if not final_text:
|
||||||
final_text = latest_interim.strip()
|
final_text = latest_interim.strip()
|
||||||
self.transcript = final_text
|
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
|
return final_text
|
||||||
|
|
||||||
def listen(
|
def listen(
|
||||||
@@ -622,10 +685,20 @@ class SpeechRecognizer:
|
|||||||
# Закрываем соединение, если оно было создано
|
# Закрываем соединение, если оно было создано
|
||||||
if dg_connection:
|
if dg_connection:
|
||||||
try:
|
try:
|
||||||
dg_connection.finish()
|
self._run_blocking_cleanup_sync(
|
||||||
|
dg_connection.finish,
|
||||||
|
timeout_seconds=DEEPGRAM_FINISH_TIMEOUT_SECONDS,
|
||||||
|
label="Deepgram finish (error cleanup)",
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
pass # Игнорируем ошибки при завершении
|
pass # Игнорируем ошибки при завершении
|
||||||
|
|
||||||
|
# Принудительно сбрасываем клиента, чтобы след. попытка не унаследовала
|
||||||
|
# подвисшее соединение SDK.
|
||||||
|
self.dg_client = None
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
self.initialize()
|
||||||
|
|
||||||
if attempt < 2: # Не ждем после последней попытки
|
if attempt < 2: # Не ждем после последней попытки
|
||||||
print(f"⚠️ Не удалось подключиться к Deepgram, попытка {attempt + 1}/3, повторяю...")
|
print(f"⚠️ Не удалось подключиться к Deepgram, попытка {attempt + 1}/3, повторяю...")
|
||||||
time.sleep(1) # Уменьшаем задержку между попытками
|
time.sleep(1) # Уменьшаем задержку между попытками
|
||||||
|
|||||||
@@ -147,6 +147,73 @@ def numbers_to_words(text: str) -> str:
|
|||||||
|
|
||||||
preps_list = "|".join(map(re.escape, PREPOSITION_CASES.keys()))
|
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):
|
def replace_year_suffix_match(match):
|
||||||
prep = match.group(1)
|
prep = match.group(1)
|
||||||
|
|||||||
@@ -335,6 +335,7 @@ def main():
|
|||||||
|
|
||||||
if not user_text:
|
if not user_text:
|
||||||
# Молчание — возвращаемся к ожиданию
|
# Молчание — возвращаемся к ожиданию
|
||||||
|
print("user was not talking")
|
||||||
skip_wakeword = False
|
skip_wakeword = False
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user