После ответа Python сразу уходит в record() ждать follow-up (FOLLOWUP_TIMEOUT), но планшет об этом не знал — оверлей тихо скрывался и пользователю казалось что Cosmo его не слышит без повторного wake-word. Теперь между итерациями _conversation_loop шлётся notifier.listening() — планшет показывает мягко пульсирующий орб с 'жду' + сохранённым текстом прошлого ответа. Закрывается только по notifier.idle() (таймаут тишины) или если пользователь что-то сказал (command).
81 lines
2.8 KiB
Python
81 lines
2.8 KiB
Python
"""
|
||
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", "")
|
||
|
||
# Когда True — локальный speak() пропускается, голос идёт через планшет.
|
||
# По умолчанию включено если TABLET_URL и VOICE_API_KEY заполнены;
|
||
# явно отключить: TABLET_TTS_ENABLED=false
|
||
TABLET_TTS_ENABLED = (
|
||
bool(TABLET_URL and VOICE_API_KEY)
|
||
and os.getenv("TABLET_TTS_ENABLED", "true").lower() in ("true", "1", "yes", "on")
|
||
)
|
||
|
||
# Переиспользуем HTTP сессию (keep-alive) для минимума latency
|
||
_session = requests.Session()
|
||
|
||
_ENABLED = bool(TABLET_URL and VOICE_API_KEY)
|
||
if _ENABLED:
|
||
tts_where = "планшет" if TABLET_TTS_ENABLED else "локально"
|
||
print(f"🔔 Notifier: события → {TABLET_URL}, TTS: {tts_where}")
|
||
else:
|
||
print("🔕 Notifier: отключён (нет TABLET_URL или VOICE_API_KEY в .env)")
|
||
|
||
|
||
def speak_locally() -> bool:
|
||
"""True если локальный speak() должен работать (TTS на этой машине)."""
|
||
return not TABLET_TTS_ENABLED
|
||
|
||
|
||
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)
|
||
|
||
|
||
def listening(agent_id: str):
|
||
"""Голосовой ассистент слушает follow-up (после ответа) — планшет показывает
|
||
мягкую пульсацию, сохраняя текст предыдущего ответа."""
|
||
_send("listening", agent=agent_id)
|