Edit tts mode
This commit is contained in:
@@ -22,7 +22,7 @@ AUDIO_SINK=
|
|||||||
|
|
||||||
# TTS (ElevenLabs)
|
# TTS (ElevenLabs)
|
||||||
ELEVENLABS_API_KEY=your_elevenlabs_api_key_here
|
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
|
COSMO_TTS_VOICE=your_cosmo_voice_id
|
||||||
LUSYA_TTS_VOICE=your_lusya_voice_id
|
LUSYA_TTS_VOICE=your_lusya_voice_id
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
numpy
|
numpy<2
|
||||||
|
|
||||||
# Audio I/O
|
# Audio I/O
|
||||||
pyaudio
|
pyaudio
|
||||||
sounddevice
|
sounddevice
|
||||||
scipy
|
scipy<1.15
|
||||||
|
|
||||||
# STT через облако
|
# STT через облако
|
||||||
groq
|
groq
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ AGENTS = {
|
|||||||
"token": GATEWAY_TOKEN,
|
"token": GATEWAY_TOKEN,
|
||||||
"agent": AGENT,
|
"agent": AGENT,
|
||||||
"voice_model": VOICE_MODEL,
|
"voice_model": VOICE_MODEL,
|
||||||
|
"session_key": os.getenv("COSMO_SESSION_KEY", "voice:home:cosmo"),
|
||||||
"tts_voice": os.getenv("COSMO_TTS_VOICE", ""),
|
"tts_voice": os.getenv("COSMO_TTS_VOICE", ""),
|
||||||
"session": _make_session(GATEWAY_TOKEN),
|
"session": _make_session(GATEWAY_TOKEN),
|
||||||
},
|
},
|
||||||
@@ -58,6 +59,7 @@ AGENTS = {
|
|||||||
"token": LUSYA_GATEWAY_TOKEN,
|
"token": LUSYA_GATEWAY_TOKEN,
|
||||||
"agent": LUSYA_AGENT,
|
"agent": LUSYA_AGENT,
|
||||||
"voice_model": LUSYA_VOICE_MODEL,
|
"voice_model": LUSYA_VOICE_MODEL,
|
||||||
|
"session_key": os.getenv("LUSYA_SESSION_KEY", "voice:home:lusya"),
|
||||||
"tts_voice": os.getenv("LUSYA_TTS_VOICE", ""),
|
"tts_voice": os.getenv("LUSYA_TTS_VOICE", ""),
|
||||||
"session": _make_session(LUSYA_GATEWAY_TOKEN),
|
"session": _make_session(LUSYA_GATEWAY_TOKEN),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,8 +8,18 @@ from .config import AGENTS, log
|
|||||||
from .text import clean_for_speech, find_sentence_end
|
from .text import clean_for_speech, find_sentence_end
|
||||||
from .tts import speak, play_error_sound
|
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"))
|
MAX_HISTORY = int(os.getenv("MAX_HISTORY", "20"))
|
||||||
|
# "stream" — режем по предложениям (быстро, но рваная интонация)
|
||||||
|
# "full" — собираем весь ответ, потом TTS (естественно, но пауза перед началом)
|
||||||
|
TTS_MODE = os.getenv("TTS_MODE", "full")
|
||||||
|
|
||||||
RESET_PATTERNS = re.compile(
|
RESET_PATTERNS = re.compile(
|
||||||
r"(начни|начать|создай|открой|давай).{0,10}(новую|новый|чистую|чистый).{0,10}(сессию|сессия|диалог|разговор|чат)"
|
r"(начни|начать|создай|открой|давай).{0,10}(новую|новый|чистую|чистый).{0,10}(сессию|сессия|диалог|разговор|чат)"
|
||||||
@@ -65,7 +75,10 @@ def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: st
|
|||||||
try:
|
try:
|
||||||
resp = session.post(
|
resp = session.post(
|
||||||
f"{gateway_url}/v1/chat/completions",
|
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={
|
json={
|
||||||
"model": agent,
|
"model": agent,
|
||||||
"stream": True,
|
"stream": True,
|
||||||
@@ -115,13 +128,14 @@ def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: st
|
|||||||
full_text += delta
|
full_text += delta
|
||||||
buffer += delta
|
buffer += delta
|
||||||
|
|
||||||
last_punct = find_sentence_end(buffer, min_len=60)
|
if TTS_MODE == "stream":
|
||||||
if last_punct > -1:
|
last_punct = find_sentence_end(buffer, min_len=120)
|
||||||
sentence = clean_for_speech(buffer[:last_punct + 1])
|
if last_punct > -1:
|
||||||
if sentence.strip():
|
sentence = clean_for_speech(buffer[:last_punct + 1])
|
||||||
print(f"🔊 Говорю: {sentence}")
|
if sentence.strip():
|
||||||
speak(sentence, agent_id)
|
print(f"🔊 Говорю: {sentence}")
|
||||||
buffer = buffer[last_punct + 1:].lstrip()
|
speak(sentence, agent_id)
|
||||||
|
buffer = buffer[last_punct + 1:].lstrip()
|
||||||
|
|
||||||
except (json.JSONDecodeError, KeyError, IndexError):
|
except (json.JSONDecodeError, KeyError, IndexError):
|
||||||
continue
|
continue
|
||||||
@@ -129,17 +143,24 @@ def ask_agent_stream(text: str, conv: "Conversation | None" = None, agent_id: st
|
|||||||
log.exception("Ошибка при чтении стрима")
|
log.exception("Ошибка при чтении стрима")
|
||||||
print(f"⚠️ Стрим прервался: {e}")
|
print(f"⚠️ Стрим прервался: {e}")
|
||||||
|
|
||||||
# Остаток
|
|
||||||
if buffer.strip():
|
|
||||||
sentence = clean_for_speech(buffer)
|
|
||||||
if sentence:
|
|
||||||
speak(sentence, agent_id)
|
|
||||||
|
|
||||||
if not full_text:
|
if not full_text:
|
||||||
msg = "Не получил ответ, попробуй ещё раз."
|
msg = "Не получил ответ, попробуй ещё раз."
|
||||||
speak(msg, agent_id)
|
speak(msg, agent_id)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
result = clean_for_speech(full_text)
|
result = clean_for_speech(full_text)
|
||||||
|
|
||||||
|
if TTS_MODE == "full":
|
||||||
|
# LLM уже доcтримил — озвучиваем весь ответ одним куском с цельной интонацией
|
||||||
|
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)
|
conv.add_assistant(full_text)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -1,12 +1,65 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
# Единицы измерения со слэшем — раскрываем до чтения слэша
|
||||||
|
UNIT_SLASH = [
|
||||||
|
(r'\bкм\s*/\s*ч\b', 'километров в час'),
|
||||||
|
(r'\bм\s*/\s*с\b', 'метров в секунду'),
|
||||||
|
(r'\bкм\s*/\s*с\b', 'километров в секунду'),
|
||||||
|
(r'\bмб\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:
|
def clean_for_speech(text: str) -> str:
|
||||||
text = re.sub(r'\*+', '', text) # убрать **жирный**
|
text = re.sub(r'\*+', '', text) # убрать **жирный**
|
||||||
text = re.sub(r'#+\s', '', text) # убрать ## заголовки
|
text = re.sub(r'#+\s', '', text) # убрать ## заголовки
|
||||||
text = re.sub(r'- ', '', text) # убрать тире списков
|
text = re.sub(r'- ', '', text) # убрать тире списков
|
||||||
text = re.sub(r'\[.*?\]\(.*?\)', '', text) # убрать ссылки
|
text = re.sub(r'\[.*?\]\(.*?\)', '', text) # убрать ссылки
|
||||||
text = re.sub(r'\n+', '. ', 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'\bт\.\s*е\.', 'то есть'),
|
||||||
|
(r'\bт\.\s*к\.', 'так как'),
|
||||||
|
(r'\bт\.\s*д\.', 'так далее'),
|
||||||
|
(r'\bт\.\s*п\.', 'тому подобное'),
|
||||||
|
(r'\bи\s*т\.\s*д\.', 'и так далее'),
|
||||||
|
(r'\bи\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'\s+', ' ', text) # лишние пробелы
|
||||||
text = re.sub(r'(\d+)\.(\s)', r'\1\2', text)
|
text = re.sub(r'(\d+)\.(\s)', r'\1\2', text)
|
||||||
return text.strip()
|
return text.strip()
|
||||||
|
|||||||
@@ -70,11 +70,11 @@ def _speak_elevenlabs(text: str, agent_id: str):
|
|||||||
return
|
return
|
||||||
|
|
||||||
voice_settings = VoiceSettings(
|
voice_settings = VoiceSettings(
|
||||||
stability=0.5,
|
stability=0.65, # ниже = живее интонация (для multilingual_v2)
|
||||||
similarity_boost=0.75,
|
similarity_boost=0.6,
|
||||||
style=0.0,
|
style=0.45, # выше = эмоциональнее
|
||||||
use_speaker_boost=True,
|
use_speaker_boost=True,
|
||||||
speed=1.1 # Значение от 0.7 до 1.2. 1.1 — это ускорение на 10%
|
speed=1.05
|
||||||
)
|
)
|
||||||
|
|
||||||
audio_stream = client.text_to_speech.convert(
|
audio_stream = client.text_to_speech.convert(
|
||||||
|
|||||||
Reference in New Issue
Block a user