Files
home-voice-assistant/satellite/modes.py
Daniil Klimov 7ca8268b78 Initial commit: Cosmo Voice Satellite
Two-agent voice assistant (Cosmo + Люся) via OpenClaw Gateway.
Streaming STT (Groq) + LLM + TTS (ElevenLabs) pipeline with
keep-alive sessions, barge-in, and daily conversation sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:34:08 +03:00

171 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import sys
from .config import GATEWAY_URL, AGENT, FOLLOWUP_TIMEOUT, log
from .audio import record, record_with_timeout
from .tts import play_activation_sound, speak, stop_speaking
from .llm import ask_agent_stream, Conversation, is_reset_command
# Персистентные сессии — одна на день для каждого агента
_sessions: dict[str, Conversation] = {}
def _get_session(agent_id: str) -> Conversation:
"""Возвращает текущую сессию, создаёт новую если день сменился"""
conv = _sessions.get(agent_id)
if conv is None or conv.is_expired():
conv = Conversation(agent_id=agent_id)
_sessions[agent_id] = conv
print(f"🆕 Новая сессия для {agent_id}")
return conv
def _handle_reset(text: str, agent_id: str) -> bool:
"""Проверяет команду сброса. Возвращает True если сброс произошёл."""
if is_reset_command(text):
_sessions[agent_id] = Conversation(agent_id=agent_id)
msg = "Начинаю новую сессию."
print(f"🔄 {msg}")
speak(msg, agent_id)
return True
return False
def run_with_enter():
print("\n🦞 Cosmo Satellite запущен (режим: Enter для активации)")
print(f" Gateway : {GATEWAY_URL}")
print(f" Агент : {AGENT}")
print("\nНажми Enter → говори → получи ответ. Ctrl+C для выхода.\n")
while True:
try:
input("⏎ Нажми Enter и говори...")
stop_speaking() # barge-in: прервать если ещё говорит
play_activation_sound()
conv = _get_session("cosmo")
while True:
text = record()
if not text:
print("⚠️ Ничего не распознано")
break
print(f"📝 Ты: {text}")
if _handle_reset(text, "cosmo"):
conv = _get_session("cosmo")
break
response = ask_agent_stream(text, conv=conv)
print(f"🤖 Cosmo: {response}\n")
print(f"👂 Слушаю продолжение ({int(FOLLOWUP_TIMEOUT)} сек)...")
followup = record_with_timeout(timeout=FOLLOWUP_TIMEOUT)
if not followup:
print("😴 Нет продолжения, жду активации...\n")
break
text = followup
except KeyboardInterrupt:
print("\n👋 Выход")
break
except Exception as e:
log.exception("Непредвиденная ошибка в цикле Enter")
print(f"⚠️ Ошибка: {e} — продолжаю работу...\n")
def run_with_porcupine():
"""Режим продакшн — два wake word через Porcupine (для Pi)"""
import pvporcupine
import struct
from .config import AGENTS
porcupine_key = os.getenv("PORCUPINE_KEY")
wake_word_cosmo = os.getenv("WAKE_WORD_COSMO")
wake_word_lusya = os.getenv("WAKE_WORD_LUSYA")
if not porcupine_key:
print("❌ PORCUPINE_KEY не задан в .env")
sys.exit(1)
keyword_paths = []
wake_word_map = []
if wake_word_cosmo:
keyword_paths.append(wake_word_cosmo)
wake_word_map.append("cosmo")
if wake_word_lusya:
keyword_paths.append(wake_word_lusya)
wake_word_map.append("lusya")
if not keyword_paths:
print("❌ WAKE_WORD_COSMO или WAKE_WORD_LUSYA не заданы в .env")
sys.exit(1)
import pyaudio
porcupine = pvporcupine.create(
access_key=porcupine_key,
keyword_paths=keyword_paths,
)
audio = pyaudio.PyAudio()
stream = audio.open(
rate=porcupine.sample_rate,
channels=1,
format=pyaudio.paInt16,
input=True,
frames_per_buffer=porcupine.frame_length,
)
print("\n🦞 Cosmo Satellite запущен (режим: wake word)")
for agent_id in wake_word_map:
cfg = AGENTS[agent_id]
print(f" {cfg['name']:6s} : {cfg['gateway_url']}{cfg['agent']}")
print(f"\nСкажи 'Космо' или 'Люся'...\n")
try:
while True:
try:
pcm = stream.read(porcupine.frame_length)
pcm = struct.unpack_from("h" * porcupine.frame_length, pcm)
keyword_index = porcupine.process(pcm)
if keyword_index >= 0:
agent_id = wake_word_map[keyword_index]
agent_name = AGENTS[agent_id]["name"]
stop_speaking() # barge-in: прервать если ещё говорит
print(f"✅ Услышал '{agent_name}'!")
play_activation_sound()
conv = _get_session(agent_id)
text = record()
if not text:
continue
print(f"📝 Ты → {agent_name}: {text}")
if _handle_reset(text, agent_id):
continue
response = ask_agent_stream(text, conv=conv, agent_id=agent_id)
print(f"🤖 {agent_name}: {response}\n")
except KeyboardInterrupt:
raise
except Exception as e:
log.exception("Непредвиденная ошибка в цикле Porcupine")
print(f"⚠️ Ошибка: {e} — продолжаю слушать...\n")
except KeyboardInterrupt:
print("\n👋 Выход")
finally:
stream.stop_stream()
audio.terminate()
porcupine.delete()