feat(notifier): route TTS to tablet when TABLET_TTS_ENABLED

When TABLET_URL and VOICE_API_KEY are set, the tablet handles TTS
via its ElevenLabs proxy — local speak() is skipped. Controlled
by TABLET_TTS_ENABLED (default true when tablet is configured).

- notifier.speak_locally() — gate used by all local speech paths
- llm._maybe_speak — no-op when tablet plays the voice
- modes._handle_reset — emits response event and skips local speak
  when tablet TTS is on; keeps spoken fallback otherwise

Tablet side in smart-home-tablet repo: /api/voice/tts endpoint +
VoiceOverlay audio playback (commit ba2e… pending).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cosmo
2026-04-23 12:52:34 +00:00
parent e4e7529063
commit 584e21923c
4 changed files with 26 additions and 3 deletions

View File

@@ -54,3 +54,8 @@ LUSYA_SESSION_KEY=agent:wife:voice:home
# Если не настроено, просто пропускается, ассистент работает как раньше.
TABLET_URL=https://tablet.digital-home.site
VOICE_API_KEY=your_voice_api_key_here
# TABLET_TTS_ENABLED=true (по умолчанию true когда TABLET_URL/KEY заданы) —
# голос ассистента проигрывается на планшете через ElevenLabs proxy,
# локальный mpv/speak пропускается. false = говорим локально как раньше.
TABLET_TTS_ENABLED=true

View File

@@ -67,7 +67,8 @@ def _post_with_retry(session, url, headers, payload):
def ask_agent_stream(text: str, agent_id: str = "cosmo") -> str:
"""Отправляет запрос к OpenClaw gateway и озвучивает ответ."""
def _maybe_speak(t: str):
if t.strip():
# Если TTS на планшете — пропускаем локальный звук, планшет зачитает по response event.
if t.strip() and notifier.speak_locally():
speak(t, agent_id)
cfg = AGENTS.get(agent_id, AGENTS["cosmo"])

View File

@@ -34,6 +34,9 @@ def _handle_reset(text: str, agent_id: str) -> bool:
msg = "Начинаю новую сессию."
print(f"🔄 {msg}")
# Отправляем как response event — tablet зачитает, локально говорим только если TTS на этой машине.
notifier.response(msg, agent_id)
if notifier.speak_locally():
speak(msg, agent_id)
return True

View File

@@ -16,16 +16,30 @@ 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:
print(f"🔔 Notifier: планшет {TABLET_URL}")
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