diff --git a/.env.example b/.env.example index 0ee1a2d..3c237c5 100644 --- a/.env.example +++ b/.env.example @@ -47,3 +47,10 @@ LOG_FILE=errors.log COSMO_SESSION_KEY=agent:voice:voice:home LUSYA_SESSION_KEY=agent:wife:voice:home + +# Smart Home Tablet integration (опционально) +# Если настроено — скрипт шлёт события состояния (wake/command/response/idle/error) +# на планшет, который показывает оверлей с Siri-blob + распознанным текстом. +# Если не настроено, просто пропускается, ассистент работает как раньше. +TABLET_URL=https://tablet.digital-home.site +VOICE_API_KEY=your_voice_api_key_here diff --git a/satellite/llm.py b/satellite/llm.py index 796c4fb..faa9f31 100644 --- a/satellite/llm.py +++ b/satellite/llm.py @@ -7,6 +7,7 @@ import requests from .config import AGENTS, VOICE_MAX_TOKENS, LLM_RETRIES, log from .text import clean_for_speech, find_sentence_end from .tts import speak, play_error_sound +from . import notifier VOICE_SESSION_KEY = os.getenv("VOICE_SESSION_KEY", "agent:main:voice:home") @@ -91,6 +92,7 @@ def ask_agent_stream(text: str, agent_id: str = "cosmo") -> str: msg = "Не могу связаться с сервером, попробуй ещё раз." print(f"⚠️ {msg}") play_error_sound() + notifier.error(msg, agent_id) _maybe_speak(msg) return msg except requests.Timeout: @@ -98,6 +100,7 @@ def ask_agent_stream(text: str, agent_id: str = "cosmo") -> str: msg = "Сервер не ответил вовремя, попробуй ещё раз." print(f"⚠️ {msg}") play_error_sound() + notifier.error(msg, agent_id) _maybe_speak(msg) return msg except requests.HTTPError as e: @@ -107,6 +110,7 @@ def ask_agent_stream(text: str, agent_id: str = "cosmo") -> str: msg = "Ошибка сервера, попробуй ещё раз." print(f"⚠️ Gateway {status}: {body[:200]}") play_error_sound() + notifier.error(msg, agent_id) _maybe_speak(msg) return msg diff --git a/satellite/modes.py b/satellite/modes.py index 90a9906..e192da0 100644 --- a/satellite/modes.py +++ b/satellite/modes.py @@ -4,6 +4,7 @@ from .config import GATEWAY_URL, AGENTS, FOLLOWUP_TIMEOUT, MAX_DURATION, log from .audio import record from .tts import speak, stop_speaking from .llm import ask_agent_stream, is_reset_command, VOICE_SESSION_KEY +from . import notifier WAKE_THRESHOLD = float(os.getenv("WAKE_THRESHOLD", "0.5")) @@ -47,15 +48,18 @@ def _conversation_loop(agent_id: str, agent_name: str = "Cosmo"): text = record(initial_silence_timeout=timeout) if not text: print("😴 Тишина, жду активации...\n") + notifier.idle() return print(f"📝 Ты → {agent_name}: {text}") + notifier.command(text, agent_id) if _handle_reset(text, agent_id): continue response = ask_agent_stream(text, agent_id=agent_id) print(f"🤖 {agent_name}: {response}\n") + notifier.response(response, agent_id) def run_with_enter(): @@ -67,6 +71,7 @@ def run_with_enter(): try: input("⏎ Нажми Enter и говори...") stop_speaking() # barge-in + notifier.wake("cosmo") _conversation_loop("cosmo", "Cosmo") except KeyboardInterrupt: @@ -110,7 +115,9 @@ def run_with_porcupine(): print(f"PREDICTION cosmo: {cosmo_score:.3f}") if cosmo_score > WAKE_THRESHOLD: + print("✅ Услышал 'Космо'!") stop_speaking() # на случай если TTS ещё играет + notifier.wake("cosmo") stream.stop_stream() _conversation_loop("cosmo", "Cosmo") cosmo_model.reset() diff --git a/satellite/notifier.py b/satellite/notifier.py new file mode 100644 index 0000000..b91c7e7 --- /dev/null +++ b/satellite/notifier.py @@ -0,0 +1,60 @@ +""" +Tablet notifier — пересылает состояния ассистента в Smart Home Tablet +(https://tablet.digital-home.site/api/voice/event). + +Планшет показывает оверлей (Siri-blob, распознанный текст, ответ). +Не критичный слой: любые сетевые ошибки глотаются, ассистент продолжает +работать даже если планшет оффлайн / не настроен. + +Активируется только когда заполнены TABLET_URL и VOICE_API_KEY в .env. +""" +import os +import requests + +from .config import log + +TABLET_URL = os.getenv("TABLET_URL", "").rstrip("/") +VOICE_API_KEY = os.getenv("VOICE_API_KEY", "") + +# Переиспользуем HTTP сессию (keep-alive) для минимума latency +_session = requests.Session() + +_ENABLED = bool(TABLET_URL and VOICE_API_KEY) +if _ENABLED: + print(f"🔔 Notifier: планшет {TABLET_URL}") +else: + print("🔕 Notifier: отключён (нет TABLET_URL или VOICE_API_KEY в .env)") + + +def _send(event: str, **payload): + if not _ENABLED: + return + try: + _session.post( + f"{TABLET_URL}/api/voice/event", + json={"event": event, **payload}, + headers={"Authorization": f"Bearer {VOICE_API_KEY}"}, + timeout=1.5, + ) + except requests.RequestException: + log.debug("Tablet notify failed (non-fatal)", exc_info=True) + + +def wake(agent_id: str): + _send("wake", agent=agent_id) + + +def command(text: str, agent_id: str): + _send("command", text=text, agent=agent_id) + + +def response(text: str, agent_id: str): + _send("response", text=text, agent=agent_id) + + +def idle(): + _send("idle") + + +def error(text: str, agent_id: str = "cosmo"): + _send("error", text=text, agent=agent_id)