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>
This commit is contained in:
170
satellite/modes.py
Normal file
170
satellite/modes.py
Normal file
@@ -0,0 +1,170 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user