Speak AI responses during OpenRouter streaming
This commit is contained in:
113
app/main.py
113
app/main.py
@@ -9,6 +9,8 @@ import time
|
||||
from collections import deque
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import queue
|
||||
import threading
|
||||
|
||||
# Для воспроизведения звуков (mp3)
|
||||
try:
|
||||
@@ -715,27 +717,110 @@ def main():
|
||||
full_response = ""
|
||||
interrupted = False
|
||||
|
||||
try:
|
||||
# Streaming от AI
|
||||
stream_generator = ask_ai_stream(list(chat_history))
|
||||
# Streaming TTS: читаем SSE без блокировок, а озвучиваем в отдельном потоке по предложениям.
|
||||
tts_queue: "queue.Queue[str | None]" = queue.Queue()
|
||||
stop_streaming_event = threading.Event()
|
||||
|
||||
print("🤖 AI: ", end="", flush=True)
|
||||
def _split_speakable(text: str) -> tuple[str, str]:
|
||||
"""
|
||||
Возвращает (готовое_для_озвучивания, остаток).
|
||||
Стараемся говорить по предложениям, но не режем слишком мелко.
|
||||
"""
|
||||
if not text:
|
||||
return "", ""
|
||||
|
||||
for chunk in stream_generator:
|
||||
full_response += chunk
|
||||
print(chunk, end="", flush=True)
|
||||
# Ждем хотя бы немного текста, чтобы не "пиликать" по 1-2 словам.
|
||||
min_chars = 55
|
||||
hard_flush_chars = 220
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Ошибка: {e}")
|
||||
speak("Произошла ошибка при получении ответа.")
|
||||
if len(text) < min_chars and "\n" not in text:
|
||||
return "", text
|
||||
|
||||
# Находим границу предложения.
|
||||
boundary = -1
|
||||
for i, ch in enumerate(text):
|
||||
if ch == "\n":
|
||||
boundary = i
|
||||
elif ch in ".!?":
|
||||
# Не режем 3.14 и похожие случаи.
|
||||
prev_is_digit = i > 0 and text[i - 1].isdigit()
|
||||
next_is_digit = i + 1 < len(text) and text[i + 1].isdigit()
|
||||
if ch == "." and prev_is_digit and next_is_digit:
|
||||
continue
|
||||
boundary = i
|
||||
|
||||
if boundary == -1:
|
||||
if len(text) >= hard_flush_chars:
|
||||
boundary = hard_flush_chars - 1
|
||||
else:
|
||||
clean_ai_response = clean_response(full_response, language="ru")
|
||||
if clean_ai_response.strip():
|
||||
interrupted = not speak(
|
||||
clean_ai_response,
|
||||
return "", text
|
||||
|
||||
speak_part = text[: boundary + 1].strip()
|
||||
rest = text[boundary + 1 :].lstrip()
|
||||
return speak_part, rest
|
||||
|
||||
def _tts_worker():
|
||||
nonlocal interrupted
|
||||
while True:
|
||||
item = tts_queue.get()
|
||||
if item is None:
|
||||
return
|
||||
if stop_streaming_event.is_set():
|
||||
continue
|
||||
|
||||
clean_part = clean_response(item, language="ru")
|
||||
if not clean_part.strip():
|
||||
continue
|
||||
|
||||
ok = speak(
|
||||
clean_part,
|
||||
check_interrupt=check_wakeword_once,
|
||||
language="ru",
|
||||
)
|
||||
if not ok:
|
||||
interrupted = True
|
||||
stop_streaming_event.set()
|
||||
# Опустошим очередь, чтобы не озвучивать "хвост" после прерывания.
|
||||
try:
|
||||
while True:
|
||||
tts_queue.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
return
|
||||
|
||||
tts_thread = threading.Thread(target=_tts_worker, daemon=True)
|
||||
tts_thread.start()
|
||||
|
||||
print("🤖 AI: ", end="", flush=True)
|
||||
|
||||
try:
|
||||
stream_generator = ask_ai_stream(list(chat_history))
|
||||
buffer = ""
|
||||
for chunk in stream_generator:
|
||||
if stop_streaming_event.is_set():
|
||||
break
|
||||
if not chunk:
|
||||
continue
|
||||
full_response += chunk
|
||||
buffer += chunk
|
||||
print(chunk, end="", flush=True)
|
||||
|
||||
while True:
|
||||
speak_part, buffer = _split_speakable(buffer)
|
||||
if not speak_part:
|
||||
break
|
||||
tts_queue.put(speak_part)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Ошибка: {e}")
|
||||
tts_queue.put("Произошла ошибка при получении ответа.")
|
||||
finally:
|
||||
# Договорим остаток, если не было прерывания.
|
||||
if not stop_streaming_event.is_set():
|
||||
tail = buffer.strip()
|
||||
if tail:
|
||||
tts_queue.put(tail)
|
||||
tts_queue.put(None)
|
||||
tts_thread.join(timeout=20)
|
||||
|
||||
print()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user