Edit tts mode
This commit is contained in:
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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,13 +128,14 @@ 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 last_punct > -1:
|
||||
sentence = clean_for_speech(buffer[:last_punct + 1])
|
||||
if sentence.strip():
|
||||
print(f"🔊 Говорю: {sentence}")
|
||||
speak(sentence, agent_id)
|
||||
buffer = buffer[last_punct + 1:].lstrip()
|
||||
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():
|
||||
print(f"🔊 Говорю: {sentence}")
|
||||
speak(sentence, agent_id)
|
||||
buffer = buffer[last_punct + 1:].lstrip()
|
||||
|
||||
except (json.JSONDecodeError, KeyError, IndexError):
|
||||
continue
|
||||
@@ -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 уже до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)
|
||||
return result
|
||||
|
||||
@@ -1,12 +1,65 @@
|
||||
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:
|
||||
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'\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'(\d+)\.(\s)', r'\1\2', text)
|
||||
return text.strip()
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user