一键导入
hermes-agentvibes-hook
Auto-speak every Hermes response via AgentVibes TTS on a remote machine over SSH. Portable skill — works with any AgentVibes setup.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Auto-speak every Hermes response via AgentVibes TTS on a remote machine over SSH. Portable skill — works with any AgentVibes setup.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Send TTS text from Hermes to a Windows laptop or Android device via AgentVibes queue-based player over SSH
🎤 AgentVibes Voice Management - Manage your text-to-speech voices across multiple providers (Piper TTS, Piper, macOS Say). Switch voices, preview audio, manage providers, add custom voices, and control TTS output.
Push the LLM to reconsider, refine, and improve its recent output. Use when user asks for deeper critique or mentions a known deeper critique method, e.g. socratic, first principles, pre-mortem, red team.
Strategic business analyst and requirements expert. Use when the user asks to talk to Mary or requests the business analyst.
System architect and technical design leader. Use when the user asks to talk to Winston or requests the architect.
Senior software engineer for story execution and code implementation. Use when the user asks to talk to Amelia or requests the developer agent.
| name | hermes-agentvibes-hook |
| description | Auto-speak every Hermes response via AgentVibes TTS on a remote machine over SSH. Portable skill — works with any AgentVibes setup. |
| category | tts |
Send every Hermes response as spoken audio to a remote machine via the AgentVibes SSH receiver.
Hermes (server/Docker)
└── agent:end event fires
└── hooks/agentvibes-tts/handler.py
└── strip markdown, truncate at word boundary
└── JSON payload → base64-encode
└── SSH → agentvibes-receiver@<host>:<port>
└── Remote machine queues & plays via Piper TTS
npx agentvibes install
ssh-keygen -t ed25519 -f /absolute/path/to/id_ed25519_agentvibes -N ""
Use an absolute path — tilde (~) expansion does not work inside Python subprocess argument lists.
Register the public key on the target machine's agentvibes-receiver user.
Create <hermes_home>/hooks/agentvibes-tts/HOOK.yaml:
name: agentvibes-tts
description: Send agent responses to AgentVibes TTS remotely
events:
- agent:end
Create <hermes_home>/hooks/agentvibes-tts/handler.py — update the four config constants at the top:
"""
AgentVibes TTS Hook — fires on agent:end to speak the agent's response.
Sends the response via SSH to the AgentVibes receiver using the full JSON
payload format (supports voice, project, effects metadata).
"""
import asyncio
import base64
import json
import logging
import os
import re
import subprocess
import time
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
AGENTVIBES_SSH_KEY = "/absolute/path/to/id_ed25519" # no tilde — subprocess won't expand it
AGENTVIBES_HOST = "<target-ip-or-hostname>"
AGENTVIBES_PORT = "<receiver-port>"
AGENTVIBES_USER = "agentvibes-receiver"
AGENTVIBES_VOICE = "en_US-libritts-high::Leo-8"
AGENTVIBES_PROJECT = "hermes"
MAX_CONTENT_LEN = 200 # chars of spoken content (prefix NOT counted)
PREFIX = "Hermes here, "
_KNOWN_HOSTS = "/absolute/path/to/known_hosts" # persistent across restarts
_LOG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tts-hook.log")
# ---------------------------------------------------------------------------
# File logger — writes to tts-hook.log next to handler.py, never to stdout
# ---------------------------------------------------------------------------
_log = logging.getLogger("agentvibes-tts")
if not _log.handlers:
try:
_h = logging.FileHandler(_LOG_FILE, encoding="utf-8")
_h.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
_log.addHandler(_h)
except OSError:
_log.addHandler(logging.NullHandler())
_log.setLevel(logging.INFO)
# ---------------------------------------------------------------------------
# Rate limiting — prevent queue flooding on rapid consecutive turns
# ---------------------------------------------------------------------------
_last_sent: float = 0.0
_MIN_INTERVAL_S: float = 3.0
def _tts_speak(text: str) -> None:
"""Build JSON payload and send to AgentVibes receiver over SSH."""
global _last_sent
if not text or not text.strip():
return
now = time.monotonic()
if now - _last_sent < _MIN_INTERVAL_S:
_log.info("rate-limited: %.1fs since last send — skipping", now - _last_sent)
return
_last_sent = now
# Truncate at word boundary so the spoken text never cuts mid-syllable
full_text = PREFIX + text.strip()
max_len = len(PREFIX) + MAX_CONTENT_LEN
if len(full_text) > max_len:
truncated = full_text[:max_len].rsplit(" ", 1)[0]
full_text = truncated + "..."
payload = base64.b64encode(
json.dumps({
"text": full_text,
"voice": AGENTVIBES_VOICE,
"project": AGENTVIBES_PROJECT,
"provider": "piper",
"pretext": "", # prefix already prepended above
"effects": "",
"music": "",
"volume": "",
"speed": "",
}).encode()
).decode()
cmd = [
"ssh",
"-i", AGENTVIBES_SSH_KEY,
"-o", "ConnectTimeout=5",
# accept-new: trust the host key on first connection, reject changes
# (MITM protection without interactive prompts)
"-o", "StrictHostKeyChecking=accept-new",
"-o", f"UserKnownHostsFile={_KNOWN_HOSTS}",
"-o", "BatchMode=yes",
"-p", AGENTVIBES_PORT,
f"{AGENTVIBES_USER}@{AGENTVIBES_HOST}",
payload,
]
try:
result = subprocess.run(cmd, capture_output=True, timeout=15)
if result.returncode != 0:
stderr = result.stderr.decode(errors="replace").strip()
_log.warning("ssh exit %d: %s", result.returncode, stderr or "(no stderr)")
else:
stdout = result.stdout.decode(errors="replace").strip()
_log.info("queued OK: %s", stdout)
except subprocess.TimeoutExpired:
_log.warning("ssh timed out after 15s to %s:%s", AGENTVIBES_HOST, AGENTVIBES_PORT)
except Exception as exc:
_log.warning("ssh error: %s", exc)
async def handle(event_type: str, context: dict) -> None:
"""Async handler — only processes agent:end events."""
if event_type != "agent:end":
return
response = (context.get("response") or "").strip()
if not response:
return
response = _strip_markdown(response)
loop = asyncio.get_running_loop()
await loop.run_in_executor(None, _tts_speak, response)
def _strip_markdown(text: str) -> str:
"""Remove markdown artifacts that sound unnatural in TTS."""
# Fenced code blocks — always unlistenable
text = re.sub(r"```[\s\S]*?```", "", text)
# Inline code — unwrap
text = re.sub(r"`([^`]+)`", r"\1", text)
# Bold / italic markers
text = re.sub(r"\*{1,2}([^\*]+)\*{1,2}", r"\1", text)
# Links — keep label, drop URL
text = re.sub(r"\[([^\]]+)\]\([^\)]+\)", r"\1", text)
# Emoji — strip everywhere, not just line-start
text = re.sub(
r"[\U0001F300-\U0001F9FF"
r"\U00002600-\U000027FF"
r"\U0000FE00-\U0000FE0F"
r"•→↓↑]+",
"",
text,
)
text = re.sub(r"\s+", " ", text).strip()
return text
hermes gateway restart
Check hooks/agentvibes-tts/tts-hook.log after the next agent turn. Every SSH call logs its result (exit code, receiver ACK, or error message).
| Constant | Description | Notes |
|---|---|---|
AGENTVIBES_SSH_KEY | Absolute path to SSH private key | No ~ — subprocess will not expand it |
AGENTVIBES_HOST | Hostname or IP of target machine | Tailscale IP recommended |
AGENTVIBES_PORT | SSH port of AgentVibes receiver | Set during npx agentvibes install |
AGENTVIBES_USER | SSH user on target machine | agentvibes-receiver |
AGENTVIBES_VOICE | Voice in voiceId::SpeakerName format | e.g. en_US-libritts-high::Leo-8 |
AGENTVIBES_PROJECT | Project tag shown in receiver log | hermes |
MAX_CONTENT_LEN | Max chars of spoken content | Prefix length NOT counted |
PREFIX | Spoken intro before each response | Hermes here, |
_KNOWN_HOSTS | Absolute path to persistent known_hosts | Required for accept-new to survive restarts |
_MIN_INTERVAL_S | Seconds between TTS calls | Prevents queue flooding on rapid turns |
event_type == "agent:end" — ignores all other hook eventsPREFIX, truncates at last word boundary before MAX_CONTENT_LEN chars, appends ...tts-hook.logStrictHostKeyChecking=accept-new + explicit UserKnownHostsFile protects against MITM after first connecttts-hook.log: Check HERMES_HOME env var so the hook file is found; check directory write permissionsagentvibes-receiver user_KNOWN_HOSTS file and reconnect oncetts-watcher.ps1) is running on the target machinetts-hook.log — SSH errors are now logged with full detailAgentVibes ships with an MCP server (npx -y -p agentvibes@beta agentvibes-mcp-server) with tools for voice selection, effects, background music, and mute/unmute. Install it in your Hermes MCP config to control AgentVibes with natural language commands. The MCP server must run on the target machine where speakers live.