Edit tts mode

This commit is contained in:
2026-04-13 18:33:19 +03:00
parent 780f6f0084
commit 7239f85506
6 changed files with 98 additions and 22 deletions

View File

@@ -22,7 +22,7 @@ AUDIO_SINK=
# TTS (ElevenLabs)
ELEVENLABS_API_KEY=your_elevenlabs_api_key_here
ELEVENLABS_MODEL=eleven_flash_v2_5
ELEVENLABS_MODEL=eleven_turbo_v2_5
COSMO_TTS_VOICE=your_cosmo_voice_id
LUSYA_TTS_VOICE=your_lusya_voice_id

View File

@@ -1,11 +1,11 @@
requests
python-dotenv
numpy
numpy<2
# Audio I/O
pyaudio
sounddevice
scipy
scipy<1.15
# STT через облако
groq

View File

@@ -49,6 +49,7 @@ AGENTS = {
"token": GATEWAY_TOKEN,
"agent": AGENT,
"voice_model": VOICE_MODEL,
"session_key": os.getenv("COSMO_SESSION_KEY", "voice:home:cosmo"),
"tts_voice": os.getenv("COSMO_TTS_VOICE", ""),
"session": _make_session(GATEWAY_TOKEN),
},
@@ -58,6 +59,7 @@ AGENTS = {
"token": LUSYA_GATEWAY_TOKEN,
"agent": LUSYA_AGENT,
"voice_model": LUSYA_VOICE_MODEL,
"session_key": os.getenv("LUSYA_SESSION_KEY", "voice:home:lusya"),
"tts_voice": os.getenv("LUSYA_TTS_VOICE", ""),
"session": _make_session(LUSYA_GATEWAY_TOKEN),
},

View File

@@ -8,8 +8,18 @@ from .config import AGENTS, log
from .text import clean_for_speech, find_sentence_end
from .tts import speak, play_error_sound
SYSTEM_PROMPT = "Отвечай кратко, 1-2 предложения, без markdown, без эмодзи."
SYSTEM_PROMPT = (
"Отвечай кратко, 1-2 предложения, без markdown, без эмодзи. "
"Ответ будет озвучен голосом, поэтому: "
"числа пиши прописью (двадцать три, а не 23), "
"единицы измерения пиши полностью (километров в час, а не км/ч), "
"не используй спецсимволы (+, -, /, %, °) — заменяй словами (плюс, минус, из, процентов, градусов). "
"Температуру пиши так: 'плюс девять градусов', а не '+9°C'."
)
MAX_HISTORY = int(os.getenv("MAX_HISTORY", "20"))
# "stream" — режем по предложениям (быстро, но рваная интонация)
# "full" — собираем весь ответ, потом TTS (естественно, но пауза перед началом)
TTS_MODE = os.getenv("TTS_MODE", "full")
RESET_PATTERNS = re.compile(
r"(начни|начать|создай|открой|давай).{0,10}(новую|новый|чистую|чистый).{0,10}(сессию|сессия|диалог|разговор|чат)"
@@ -65,7 +75,10 @@ def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: st
try:
resp = session.post(
f"{gateway_url}/v1/chat/completions",
headers={"x-openclaw-model": cfg["voice_model"]},
headers={
"x-openclaw-model": cfg["voice_model"],
"x-openclaw-session-key": cfg["session_key"],
},
json={
"model": agent,
"stream": True,
@@ -115,7 +128,8 @@ def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: st
full_text += delta
buffer += delta
last_punct = find_sentence_end(buffer, min_len=60)
if TTS_MODE == "stream":
last_punct = find_sentence_end(buffer, min_len=120)
if last_punct > -1:
sentence = clean_for_speech(buffer[:last_punct + 1])
if sentence.strip():
@@ -129,17 +143,24 @@ def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: st
log.exception("Ошибка при чтении стрима")
print(f"⚠️ Стрим прервался: {e}")
# Остаток
if buffer.strip():
sentence = clean_for_speech(buffer)
if sentence:
speak(sentence, agent_id)
if not full_text:
msg = "Не получил ответ, попробуй ещё раз."
speak(msg, agent_id)
return msg
result = clean_for_speech(full_text)
if TTS_MODE == "full":
# LLM уже доримил — озвучиваем весь ответ одним куском с цельной интонацией
if result.strip():
print(f"🔊 Говорю: {result}")
speak(result, agent_id)
else:
# остаток буфера в stream-режиме
if buffer.strip():
tail = clean_for_speech(buffer)
if tail:
speak(tail, agent_id)
conv.add_assistant(full_text)
return result

View File

@@ -1,12 +1,65 @@
import re
# Единицы измерения со слэшем — раскрываем до чтения слэша
UNIT_SLASH = [
(r'\bкм\s*/\s*ч\b', 'километров в час'),
(r'\\s*/\s*с\b', 'метров в секунду'),
(r'\bкм\s*/\s*с\b', 'километров в секунду'),
(r'\б\s*/\s*с\b', 'мегабит в секунду'),
(r'\bгб\s*/\s*с\b', 'гигабит в секунду'),
(r'\bруб\s*/\s*мес\b', 'рублей в месяц'),
(r'\bр\s*/\s*мес\b', 'рублей в месяц'),
]
def clean_for_speech(text: str) -> str:
text = re.sub(r'\*+', '', text) # убрать **жирный**
text = re.sub(r'#+\s', '', text) # убрать ## заголовки
text = re.sub(r'- ', '', text) # убрать тире списков
text = re.sub(r'\[.*?\]\(.*?\)', '', text) # убрать ссылки
text = re.sub(r'\n+', '. ', text) # переносы → точки
# составные единицы со слэшем — до общей замены `/`
for pat, repl in UNIT_SLASH:
text = re.sub(pat, repl, text, flags=re.IGNORECASE)
# знаки перед числом: "+9", "-3", "±2"
text = re.sub(r'(^|\s)\+(\d)', r'\1плюс \2', text)
text = re.sub(r'(^|\s)-(\d)', r'\1минус \2', text)
text = re.sub(r'±(\d)', r'плюс-минус \1', text)
# дроби и отношения "12/15" → "12 из 15", "5/10" → "5 из 10"
text = re.sub(r'(\d+)\s*/\s*(\d+)', r'\1 из \2', text)
# одиночный слэш — как союз "или"
text = text.replace('/', ' или ')
# градусы и прочие символы
text = re.sub(r'°\s*C\b', ' градусов', text, flags=re.IGNORECASE)
text = re.sub(r'°\s*F\b', ' градусов Фаренгейта', text, flags=re.IGNORECASE)
text = text.replace('°', ' градусов')
text = text.replace('%', ' процентов')
text = text.replace('', ' номер ')
text = text.replace('&', ' и ')
text = text.replace('@', ' собака ')
text = text.replace('×', ' на ')
# распространённые сокращения в полную форму — иначе TTS буквоедит
abbr = [
(r'\\.\s*е\.', 'то есть'),
(r'\\.\s*к\.', 'так как'),
(r'\\.\s*д\.', 'так далее'),
(r'\\.\s*п\.', 'тому подобное'),
(r'\\s*т\.\s*д\.', 'и так далее'),
(r'\\s*т\.\s*п\.', 'и тому подобное'),
(r'\bпо-\s*русски\b', 'по-русски'),
]
for pat, repl in abbr:
text = re.sub(pat, repl, text, flags=re.IGNORECASE)
# схлопываем дубли пунктуации
text = re.sub(r'([:;,!?])\s*\.', r'\1', text)
text = re.sub(r'\.\s*\.+', '.', text)
text = re.sub(r'\s+', ' ', text) # лишние пробелы
text = re.sub(r'(\d+)\.(\s)', r'\1\2', text)
return text.strip()

View File

@@ -70,11 +70,11 @@ def _speak_elevenlabs(text: str, agent_id: str):
return
voice_settings = VoiceSettings(
stability=0.5,
similarity_boost=0.75,
style=0.0,
stability=0.65, # ниже = живее интонация (для multilingual_v2)
similarity_boost=0.6,
style=0.45, # выше = эмоциональнее
use_speaker_boost=True,
speed=1.1 # Значение от 0.7 до 1.2. 1.1 — это ускорение на 10%
speed=1.05
)
audio_stream = client.text_to_speech.convert(