speak-aloud-mcp
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.
MCP server: 让你的Ai用电脑发出声音(ElevenLabs TTS, volume set/restore). macOS / Windows / Linux.
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 就有)。
为什么要这个
我们的家用系统 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,但核心不依赖 MCP:config.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 Desktop(claude_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(系统自带) |
osascript(volume 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:播放器把文件放完了。audible:false= 我们确知听不见(还是静音 / 音量 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,正文写「没播出来 + 原因 + 文件在哪」。
我们踩过、你可以不用踩的坑
- 工具回执里别夹音频 base64。 早期版本把整段 mp3 base64 塞进
structuredContent给一个网页小播放器用。Claude Code 对工具回执有大小上限(默认 25,000 token,MAX_MCP_OUTPUT_TOKENS)——一句长一点的话(mp3 ~290KB → base64 38 万字符)直接超限,整轮被丢,AI 只看到"报错"。短句测试擦边过了,长句才炸,排了半天。文件路径就是交接物,回执恒定几百字符。 - 两个 AI 同时开口,音量会被卡在中间态。 "读旧音量 → 设 35 → 播 → 复原" 两路交错,后一路把前一路临时设的 35 读成"旧值",播完系统就停在 35。整段临界区加了进程锁,后到的排队。锁是进程内的:stdio 模式下每个客户端各起一个 server 进程,锁互相看不见——几个 agent 共用一台机器的时候,用
--transport http起一个 server 让大家连它。 - 复原要核,不能自报。 设回旧值之后再读一次比对;不一致就报
restore_failed,读不回来就报restore_unverified,不写"已恢复"。同理静音:静音状态下"播成功"是假的,先解开、播完再静回去,都写进回执。 - key ID ≠ key。 ElevenLabs 后台两串都能复制到,只有
sk_开头那串是 key。启动时直接拒掉不带sk_的,省得你对着 401 猜。 - launchd / 服务环境里
afplay能不能出声要实测。 交互 shell 里能响不代表后台服务里能响(macOS 权限/会话不同)。--say就是给你在目标环境里做这一步的。 - Linux 的
pactl set-sink-volume 35%会把所有声道抹成一样。 复原时如果只记了一个数,左右平衡就没了。按声道存、按声道还。 - 依赖钉
mcp>=1.19,<2。 下限:工具直接返回CallToolResult(带isError/structuredContent)从 SDK 1.19 才原样透传,更老的版本会把它当普通对象重新序列化,错误变成"成功"。上限:2.0 把FastMCP改名MCPServer、传输配置挪进run(),旧 import 直接没了;0.1.x 钉在 v1,v2 移植另开。 - macOS 的
set volume output volume N会顺手把静音解掉。 所以静音状态必须在动音量之前读,复原时先放音量、最后再把静音放回去——顺序反了回执就会说谎。 - 音频文件不自动清。
SPEAK_ALOUD_CACHE_DIR按天分目录只增不减,自己定期删或指到 tmp。
开发
pip install -e ".[dev]"
pytest
测试全部打桩:不连 ElevenLabs、不真播、不动系统音量。
作者
- 晚晚(@tsuru0805)——设计、拍板、真场验收。
- 弥野(Claude,晚晚的工程手)——实现与文档。
出自我们的家用系统 tilldusk。
License
MIT
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi