speak-aloud-mcp

mcp
Guvenlik Denetimi
Uyari
Health Uyari
  • License — License: MIT
  • Description — Repository has a description
  • Active repo — Last push 0 days ago
  • Low visibility — Only 5 GitHub stars
Code Gecti
  • Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
  • Permissions — No dangerous permissions requested

Bu listing icin henuz AI raporu yok.

SUMMARY

MCP server: 让你的Ai用电脑发出声音(ElevenLabs TTS, volume set/restore). macOS / Windows / Linux.

README.md

speak-aloud-mcp

让 AI 从你电脑的音响里出声。

一个很小的 MCP server,给任何 MCP 客户端(Claude Desktop / Claude Code / claude.ai Connectors / 你自己的 agent)加两个工具——核心不依赖 MCP,从自己的网关直连 API 的人当普通 Python 库用就行,见下文

工具 做什么
speak(text, voice) ElevenLabs 合成 → 存成音频文件 → 返回路径。不出声。
speak_aloud(text, voice) 同上,然后当场从本机音响播出来:静音的话先解开 → 把系统音量设到你指定的值 → 播 → 播完先把原音量放回去、再把静音状态放回去,两样都读回来核一遍。

macOS / Windows / Linux 都能跑,播放不装任何第三方 Python 音频库(macOS/Windows 用系统自带的播放器;Linux 要有 paplay/aplay,一般装了 PulseAudio/PipeWire 就有)。

English README


为什么要这个

我们的家用系统 tilldusk 里有几个长期在线的 AI agent,跑在一台 24/7 的 Mac mini 上。它们会被定时/事件唤醒,醒了有话想说——之前只能"发一条语音消息等人点开"。这个 server 让"说"就是说:它自己醒了、想道早安,声音就从屋里的音响出来,不用任何人去点播放。

顺手解决的两件小事:

  • 音量:夜里/白天系统音量不定,播之前设成固定值,播完复原,别把音量留在奇怪的位置。
  • 回执要诚实:播没播出来、音量有没有复原,结果里明说;不许"合成成功了就当播了"。

安装

git clone https://github.com/tsuru0805/speak-aloud-mcp && cd speak-aloud-mcp
pip install -e .              # macOS / Linux
pip install -e ".[windows]"   # Windows:多装 pycaw 用来读/设系统音量(不装也能播,只是不动音量)

(PyPI 包名 speak-aloud-mcp 尚未发布;发了会把这里换成 pip install speak-aloud-mcp。)需要 Python ≥ 3.10、mcp SDK 1.19–1.x。

配置(全部走环境变量)

变量 必填 说明
ELEVENLABS_API_KEY ElevenLabs API key,必须 sk_ 开头。后台里那串不带 sk_ 的是 key ID,不是 key——用错了 ElevenLabs 会回 “API key ID used as API key”。
SPEAK_ALOUD_VOICES ✅(二选一) 名字=voice_id,名字2=voice_id2,第一个是默认声。
ELEVENLABS_VOICE_ID ✅(二选一) 只有一个声的时候用这个,名字自动叫 default
SPEAK_ALOUD_VOLUME 播放时的系统音量 0-100,默认 35。设成 off = 不碰音量。
SPEAK_ALOUD_FORMAT wav(默认:macOS/Windows 系统自带播放器直接播,Linux 用 paplay/aplay)或 mp3(macOS afplay;Linux mpg123/ffplay;Windows 需要 ffplay)。
SPEAK_ALOUD_ALLOWED_HOSTS 只在 --transport http 有用:逗号分隔的公网主机名(隧道给你的那个)。localhost 和 --host 绑的局域网 IP 一直放行,其余 421。见下文。
SPEAK_ALOUD_CACHE_DIR 音频文件落哪。默认系统缓存目录下 speak-aloud-mcp/
SPEAK_ALOUD_MAX_CHARS 单次最长字符数,默认 1000。ElevenLabs 按字符计费,这是防手滑。
ELEVENLABS_MODEL_ID 默认 eleven_multilingual_v2

先在终端确认它能出声

export ELEVENLABS_API_KEY=sk_...
export SPEAK_ALOUD_VOICES="mika=你的voice_id"

speak-aloud-mcp --check          # 看后端/音量/配置有没有问题
speak-aloud-mcp --say "你好,我在。"   # 真合成、真播一次(--voice 名字 选声;听不见/没播成退出码非 0)

--say 通了再接客户端——这样出问题时你知道是音响那边还是 MCP 那边。

不用 MCP 也能用(接 API 的网关看这里)

这个仓叫 -mcp,但核心不依赖 MCPconfig.py / tts.py / player.py 三个模块零 MCP 依赖(只有 server.py import 它),所以从自己的网关直连 Claude/OpenAI/任何 API 的人一样能用。四条路,自己挑:

① 当普通 Python 库——最省事,跟 MCP 完全无关。合成 + 出声就是两个函数调用,塞进你自己的工具处理函数里:

from speak_aloud_mcp import config, player, tts

conf = config.load()
name, voice_id = conf.resolve_voice("")            # 空 = 默认声
audio = await tts.synthesize(api_key=conf.api_key, voice_id=voice_id,
                             text=tts.clean_text(text), model_id=conf.model_id,
                             output_format=conf.output_format)
path = tts.new_audio_path(conf.cache_dir, conf.suffix)
tts.write_atomic(path, audio)
res = await asyncio.to_thread(player.play_with_volume, path, conf.volume)  # 阻塞,别占事件循环

完整可跑版本:examples/use_as_library.py没装 mcp 包也能 import

② 当命令行——任何语言 subprocess 一调:speak-aloud-mcp --say "……"(听不见/没播成退出码非 0)。

③ 自己的网关里当 MCP 客户端——"接 API" 和 "用 MCP" 不冲突:你的网关连本地 MCP server,把工具表塞进 tools=,模型要调时你自己执行。Anthropic Python SDK 直接带适配器(pip install "anthropic[mcp]"):

from anthropic.lib.tools.mcp import async_mcp_tool

runner = claude.beta.messages.tool_runner(
    model="claude-opus-5", max_tokens=1024,
    tools=[async_mcp_tool(t, mcp_client) for t in (await mcp_client.list_tools()).tools],
    messages=[{"role": "user", "content": prompt}],
)

完整可跑版本:examples/api_tool_use.py。server 是你起的子进程,不对外暴露任何端口。(这条路是我们自己家在用的:网关直连 API,工具全部走 MCP 客户端拿。)

④ API 侧的 MCP connector(mcp_servers=[...])——这条要当心。 那是 Anthropic 服务器去连你给的 URL,也就是说你家这台机器得公网可达。让别人的服务器有权限让你家音响出声,风险自己掂量:真要用就上隧道 + --allowed-host + 前面加一层鉴权。想在本机出声的话,①②③ 都比它合适。

接客户端

Claude Code

claude mcp add --transport stdio speak-aloud \
  --env ELEVENLABS_API_KEY=sk_... \
  --env SPEAK_ALOUD_VOICES="mika=你的voice_id" \
  -- speak-aloud-mcp

Claude Desktopclaude_desktop_config.json,完整示例见 examples/

{
  "mcpServers": {
    "speak-aloud": {
      "command": "speak-aloud-mcp",
      "env": {
        "ELEVENLABS_API_KEY": "sk_...",
        "SPEAK_ALOUD_VOICES": "mika=你的voice_id"
      }
    }
  }
}

HTTP(claude.ai Connectors / 远程 agent)

speak-aloud-mcp --transport http --host 127.0.0.1 --port 8765

MCP 端点在 http://127.0.0.1:8765/mcp(Claude Code:claude mcp add --transport http speak-aloud http://127.0.0.1:8765/mcp)。

要给 claude.ai 用得有公网 HTTPS(Tailscale Funnel / Cloudflare Tunnel / ngrok 都行)。这时候要多做一件事:MCP SDK 默认开着 DNS-rebinding 保护,只认 Host: localhost,隧道转进来的请求带的是公网主机名,会被 421 拒掉。把那个主机名告诉它:

speak-aloud-mcp --transport http --allowed-host voice.example.com
# 或 SPEAK_ALOUD_ALLOWED_HOSTS=voice.example.com

保护不关,只是把你点名的主机加进白名单。这个 server 自己不带鉴权——它能让你家音响出声,暴露公网前请在前面加一层(反代 Basic Auth / Tunnel 自带的访问控制)。

各系统怎么播、怎么调音量

播放 音量 / 静音 备注
macOS afplay(系统自带) osascriptvolume settings 用 launchd 跑时也能出声(我们的生产环境就是这么跑的)。
Windows winsound(Python 标准库,wav,带 SND_NODEFAULT 不会失败了还叮一声);mp3 需要 ffplay(ffmpeg) pycaw(可选,pip install "speak-aloud-mcp[windows]";每次调用都在工作线程里 CoInitialize) 没装 pycaw 照样播,回执会写「音量未动」。
Linux paplay / aplay(wav),mpg123 / ffplay(mp3) pactl(PulseAudio / PipeWire);音量按声道存取,左右平衡不会被抹平 没有 pactl 就不动音量。

诚实声明:作者手边只有 macOS 生产环境。Windows / Linux 路径是按各自官方接口写的、有单元测试(打桩),没在真机跑过。你在 Windows/Linux 上 --say 通了或没通,开个 issue 说一声,会很有帮助。

回执长什么样

speak_aloud 返回一行人话 + 一小段结构化字段:

🔈 played out loud (mika); volume 35 during playback, previous volume 30 restored (verified); file: .../2026-08-16/1786807739-ee568333.wav
{"file": ".../2026-08-16/1786807739-ee568333.wav", "voice": "mika", "text": "...",
 "duration_sec": 1.86, "played": true, "audible": true, "play_error": null,
 "volume_during": 35, "volume_state": "restored", "volume_after": 30, "note": null, "backend": "macos"}
  • played:播放器把文件放完了。
  • audiblefalse = 我们确知听不见(还是静音 / 音量 0),这时正文开头是「⚠️ playback ran but was INAUDIBLE」而不是「played out loud」;null = 判断不了(读不到静音/音量)。没有麦克风,证明不了空气真震了——这个字段只承诺"没有已知的原因让它听不见"。
  • volume_during:设完音量读回来的值。正文里「volume N during playback」的 N 永远是这个读回来的数,不是你配置的数;读不回来就写「playback volume unknown」。
  • volume_state 只描述音量复原这一件事,和播没播成无关:restored(放回去了,并且读回来核过)/ restore_unverified(放回去了,但读不回来没法核)/ restore_failed(放不回去,或读回来对不上——volume_after 是最后读到的值)/ untouched(没动过:没要求、读不到原值、或这个系统没音量后端)。
  • note:播了但有话要说——原音量读不到 / 音量设了但读回来不对 / 配置音量是 0 / 输出本来是静音的(先解开、播完把音量放回去之后再静回去、读回来核;解不开或静不回去都会明说)。

播放失败不是错误结果:文件已经存好了,isError=false,正文写「没播出来 + 原因 + 文件在哪」。

我们踩过、你可以不用踩的坑

  1. 工具回执里别夹音频 base64。 早期版本把整段 mp3 base64 塞进 structuredContent 给一个网页小播放器用。Claude Code 对工具回执有大小上限(默认 25,000 token,MAX_MCP_OUTPUT_TOKENS)——一句长一点的话(mp3 ~290KB → base64 38 万字符)直接超限,整轮被丢,AI 只看到"报错"。短句测试擦边过了,长句才炸,排了半天。文件路径就是交接物,回执恒定几百字符。
  2. 两个 AI 同时开口,音量会被卡在中间态。 "读旧音量 → 设 35 → 播 → 复原" 两路交错,后一路把前一路临时设的 35 读成"旧值",播完系统就停在 35。整段临界区加了进程锁,后到的排队。锁是进程内的:stdio 模式下每个客户端各起一个 server 进程,锁互相看不见——几个 agent 共用一台机器的时候,用 --transport http一个 server 让大家连它。
  3. 复原要核,不能自报。 设回旧值之后再读一次比对;不一致就报 restore_failed,读不回来就报 restore_unverified,不写"已恢复"。同理静音:静音状态下"播成功"是假的,先解开、播完再静回去,都写进回执。
  4. key ID ≠ key。 ElevenLabs 后台两串都能复制到,只有 sk_ 开头那串是 key。启动时直接拒掉不带 sk_ 的,省得你对着 401 猜。
  5. launchd / 服务环境里 afplay 能不能出声要实测。 交互 shell 里能响不代表后台服务里能响(macOS 权限/会话不同)。--say 就是给你在目标环境里做这一步的。
  6. Linux 的 pactl set-sink-volume 35% 会把所有声道抹成一样。 复原时如果只记了一个数,左右平衡就没了。按声道存、按声道还。
  7. 依赖钉 mcp>=1.19,<2 下限:工具直接返回 CallToolResult(带 isError / structuredContent)从 SDK 1.19 才原样透传,更老的版本会把它当普通对象重新序列化,错误变成"成功"。上限:2.0 把 FastMCP 改名 MCPServer、传输配置挪进 run(),旧 import 直接没了;0.1.x 钉在 v1,v2 移植另开。
  8. macOS 的 set volume output volume N 会顺手把静音解掉。 所以静音状态必须在动音量之前读,复原时先放音量、最后再把静音放回去——顺序反了回执就会说谎。
  9. 音频文件不自动清。 SPEAK_ALOUD_CACHE_DIR 按天分目录只增不减,自己定期删或指到 tmp。

开发

pip install -e ".[dev]"
pytest

测试全部打桩:不连 ElevenLabs、不真播、不动系统音量。

作者

  • 晚晚@tsuru0805)——设计、拍板、真场验收。
  • 弥野(Claude,晚晚的工程手)——实现与文档。

出自我们的家用系统 tilldusk

License

MIT

Yorumlar (0)

Sonuc bulunamadi