先把话说出来:一个让我折腾到凌晨两点的需求
我有个 Hermes Agent,平时帮我整理邮件、查天气、管理日程,干得挺欢。但问题来了——它是个哑巴。每次我在地铁上,手机屏幕一亮,看到它给我推送的「您有 3 封新邮件,其中 2 封来自老板」,我还得掏出手机解锁、点开、看一遍。这体验太反人类了。我真正想要的是:它直接开口告诉我「老板发了封邮件,标题是催你交周报,截止时间是明天下午三点」。所以我决定给这个哑巴 Agent 装上声带,用 TTS(Text-to-Speech)让它开口说话。
一开始我以为这事儿很简单,不就是调个 API 吗?结果我错了。我踩了整整一个晚上的坑,从 Python 环境变量到音频设备权限,再到 Hermes 的配置语法,每一步都有报错等着我。这篇教程就是把我这一晚上的血泪史整理出来,每一步、每个报错、每个解决方案,全给你写清楚。你照着做,应该两小时内能跑通。我用的环境是 Ubuntu 22.04,Python 3.10.12,Hermes Agent 版本 0.4.2。你版本不一样可能报错信息略有差别,但思路通用。
咱们的目标很明确:让 Hermes Agent 在完成某个任务后(比如查完天气),用语音把结果念出来。我会用 edge-tts 这个微软的免费 TTS 库,因为它不需要注册 API key,网速够快就能用,而且音质比 espeak 那种机器人声好太多了。后面我会讲到为什么不用 OpenAI 的 TTS——不是因为它不好,是因为它要钱,而且我在国内网络环境下调用它经常超时。
第一步:装 edge-tts 并搞定第一个坑(依赖冲突)
先建个虚拟环境,别直接装全局,不然你的系统 Python 环境迟早被搞乱。我习惯用 venv,简单粗暴:
cd ~/hermes-voice
python3 -m venv venv
source venv/bin/activate
pip install edge-tts==6.1.9
这里我指定了版本号,因为 edge-tts 更新很频繁,6.2.0 之后的版本改了一些参数名,导致我的脚本报错。你如果装最新版,后面调用的时候如果遇到 TypeError: synthesis() got an unexpected keyword argument 'voice',那就是版本问题,直接降级到 6.1.9 就行。我当时就卡在这,查了半天 GitHub issues,发现好几个人跟我一样。
装完之后先跑个最简单的测试,确认它能出声:
edge-tts --text "你好,我是你的助手" --voice zh-CN-XiaoxiaoNeural --write-media test.mp3
如果这步成功了,你会得到一个 test.mp3 文件,用播放器打开能听到女声说那句话。我遇到的第一个报错是 RuntimeError: Event loop is closed,这破问题在 6.1.9 版本里基本不会出现,但如果你用最新版就有概率碰到。解决方案是降级,或者用 asyncio.run() 包一层,但我觉得降级最省事。
接下来验证音频输出设备。我在服务器上跑的时候发现没声卡,报错 ALSA lib pcm.c:2722:(snd_pcm_open_noupdate) Unknown PCM。这是因为 edge-tts 只生成 mp3,播放需要另外的工具。我用的 mpg123,装上就行:apt install mpg123。但如果你是在台式机或者笔记本上跑,自带的音频输出一般没问题。服务器的话,建议先把 mp3 文件传到本地再放,别在服务器上折腾声卡。
第二步:把 TTS 封装成 Hermes 的一个技能(技能目录结构)
Hermes Agent 的技能放在 ~/.hermes/skills/ 目录下,每个技能是一个文件夹,里面必须有一个 skill.yaml 文件描述元数据,还有一个 Python 文件实现逻辑。我建了个叫 tts_speak 的技能:
mkdir -p ~/.hermes/skills/tts_speak
cd ~/.hermes/skills/tts_speak
touch skill.yaml speak.py
skill.yaml 的内容长这样:
name: tts_speak
description: 用语音播报指定的文本内容
version: 1.0.0
author: your-name
triggers:
- speak
- 说
- 念出来
parameters:
- name: text
type: string
required: true
description: 要播报的文本
这里有个坑:triggers 里的关键词一定要用小写,而且不要加标点。我第一次写的是「说:」带着冒号,结果 Hermes 死活不触发。看日志才发现它匹配的是纯文本,不带标点的。日志在 ~/.hermes/logs/agent.log,排查问题全靠它。
speak.py 的逻辑也很简单,用 subprocess 调用 edge-tts 命令,生成临时 mp3 然后用 mpg123 播放。核心代码就这几行:
import subprocess
import tempfile
import os
def run(text: str) -> str:
tmp_file = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False)
tmp_path = tmp_file.name
tmp_file.close()
cmd = [
"edge-tts", "--text", text,
"--voice", "zh-CN-YunxiNeural",
"--write-media", tmp_path
]
subprocess.run(cmd, capture_output=True, check=True)
# 播放
subprocess.Popen(["mpg123", "-q", tmp_path])
# 清理文件(延迟删除)
os.unlink(tmp_path)
return f"已播放语音:{text[:20]}..."
注意我用的是 zh-CN-YunxiNeural 这个男声。Xiaoxiao 是女声,看个人喜好。男声听起来更沉稳一点,适合播报工作通知。Yunxi 的音色比 Xiaoxiao 稍微低沉一些,但语速默认偏快,我后面会用 --rate=-10% 调慢一点,不然听起来像在赶集。
第三步:让 Hermes 认账(配置文件和触发测试)
技能文件写好了,需要告诉 Hermes 重新加载。Hermes 的配置文件在 ~/.hermes/config.yaml,你得检查一下有没有把技能目录正确指向。我打开配置文件看到默认的 skills_dir: ~/.hermes/skills,没问题。但有个细节:如果你改了技能文件夹的名字,必须重启 Hermes 服务,它不会热加载。我第一次改完没重启,直接跟 Agent 说「speak 你好」,它回了一句「我没有这个技能」。看了日志才发现它加载的还是旧的技能列表。
重启方式是在 Hermes 的安装目录下执行 hermes restart,或者如果你是用 systemd 管理的,就 systemctl restart hermes-agent。我用的后者,因为我把 Hermes 注册成了服务,开机自启。
重启之后测试一下:
hermes chat
进入交互模式后输入:
speak 你好,我是你的语音助手
正常的话,你会听到扬声器里传出一句男声「你好,我是你的语音助手」。如果没声音,先检查 mpg123 是否安装,再检查音频设备。我遇到的一个怪问题是:声音从耳机孔出来了,但我插的是 HDMI 显示器,显示器自带扬声器。查了 aplay -l 才发现默认输出设备是耳机口。用 mpg123 -a hw:0,3 手动指定设备才解决。后来我干脆在 speak.py 里写死了设备参数:subprocess.Popen(["mpg123", "-a", "hw:0,3", "-q", tmp_path])。
这一步的报错基本集中在权限上。如果你在 Docker 容器里跑 Hermes,需要加 --device /dev/snd 挂载声卡,不然容器里根本没有音频设备,mpg123 会直接报 Cannot open audio device。我在自己笔记本上跑没这个问题,但一部署到服务器容器里就踩了。
第四步:让 Agent 主动开口(跟记忆和定时任务联动)
光能被动响应还不够,我想要的是 Agent 主动播报。比如每天早上九点,它自动告诉我今天的日程安排。Hermes 有定时任务功能,用 hermes cron create 创建。我先写好一个返回文本的脚本,再把它跟 TTS 技能串起来。
创建定时任务的命令长这样:
hermes cron create --name "morning_brief" --schedule "0 9 * * *" --task "get_daily_plan"
这里 get_daily_plan 是我之前写好的一个技能,用来汇总日历、天气、待办事项。问题来了:定时任务执行后,结果只是写入了日志,不会自动触发 TTS。我需要在 get_daily_plan 的代码里,把返回的文本同时传给 tts_speak 技能。最简单的方式是直接调用:
from hermes.skills import tts_speak
tts_speak.run(result_text)
但这样耦合太紧了,我更喜欢用 Hermes 的事件总线。不过 Hermes 0.4.2 版本的事件系统还不完善,官方文档写的是 0.5.0 才支持。所以我退而求其次,直接在技能里调用。反正都是同一个进程,没毛病。
跑了一天后发现另一个问题:mpg123 播放是阻塞的,如果定时任务同时触发多个播报,会排队播放,听起来很乱。我改成用 subprocess.Popen 非阻塞播放,并且加了个队列,每次播放前先杀掉上一个进程。代码改成:
subprocess.Popen(["pkill", "-f", "mpg123"])
subprocess.Popen(["mpg123", "-q", tmp_path])
这样粗暴但有效。不过要注意,如果你同时跑着别的音乐播放器,也会被杀掉。我目前没遇到这种情况,因为服务器上没别的播放器。
第五步:音色调优和语速控制(参数细节)
默认的 Yunxi 语速偏快,而且读数字的时候容易出错。比如「3.14」它会读成「三点一四」,但「2025年」它会读成「二零二五年」,听起来不像人话。我试了 --rate=-20% 之后,语速合适了,但数字问题还在。后来我用正则把数字预处理了一下,把阿拉伯数字转成中文大写:
import re
def preprocess(text):
# 把 2025 转成 两千零二十五
text = re.sub(r'(\d{4})年', lambda m: f"{int(m.group(1))}年", text)
# 把小数点的点改成点
text = text.replace('.', '点')
return text
但这玩意儿不够智能,比如「3.14」转成「3点14」听起来还是怪。后来我放弃了,直接用 --format audio-24khz-48kbitrate-mono-mp3 参数提高音频质量,音质好了之后,数字读错的违和感降低了不少。这个参数是 edge-tts 6.1.9 特有的,更高版本可能改名字了。
另外,我还发现 edge-tts 对中文标点的处理有问题。它会把「你好,世界」读成「你好 逗号 世界」,但用中文逗号「,」就没这个问题。坑爹。我写了个替换函数,把所有英文标点转成中文标点再传给 TTS:
text = text.replace(',', ',').replace('.', '。').replace('?', '?')
这个细节让我折腾了半小时,之前播报邮件内容的时候,英文标点被念出来,听起来特别蠢。
音色方面,我测试了三个中文声音:Xiaoxiao(女声)、Yunxi(男声)、Yunjian(男声,更低沉)。Yunjian 适合播报严肃内容,但语速更慢,我用在晚间总结上。Yunxi 用在日常提醒。你可以写个循环脚本,把同一段文字用不同声音生成,对比着听。我建议别用太多声音,一个男声一个女声足够,切换太频繁反而让人烦躁。
第六步:踩坑总结——关于网络、缓存和挂掉的服务
edge-tts 虽然免费,但它依赖微软的服务器。国内网络环境不稳定,经常超时。我一开始没做超时处理,结果 Hermes 卡在等待 TTS 响应上,整个 Agent 像死了一样。解决方案是给 subprocess 调用加超时:
subprocess.run(cmd, capture_output=True, timeout=10)
超过 10 秒就放弃,返回「语音合成超时」。另外,edge-tts 会缓存令牌,第一次调用要联网获取 token,之后会缓存到 ~/.edge-tts 目录。如果你改了系统时间,缓存会失效,重新获取就好。我遇到过缓存文件权限错误,删掉 ~/.edge-tts 重新生成就行。
还有一点,Hermes 的 memory 命令可以记录用户偏好。我让 Agent 记住了「用户喜欢男声播报、语速偏慢、播放前先打断当前音频」这些设置。这样每次调用 TTS 技能时,它会自动读取记忆里的偏好,不用每次重新指定参数。配置在 ~/.hermes/config.yaml 的 memory 段里加一条:
memory:
tts_voice: zh-CN-YunxiNeural
tts_rate: -15%
tts_interrupt: true
然后在 speak.py 里读取这些配置:
import yaml
with open(os.path.expanduser("~/.hermes/config.yaml"), "r") as f:
cfg = yaml.safe_load(f)
voice = cfg["memory"]["tts_voice"]
rate = cfg["memory"]["tts_rate"]
这套流程跑通之后,我让 Agent 每天晚上十点自动播报第二天的天气和日程。它现在每天准时开口,虽然偶尔还是会读错数字,但整体体验已经比看屏幕好太多了。我把这个技能分享给同事,他拿回去改了改,用在家庭智能音箱的对接上,也跑通了。
💬 你用过哪些AI工具?
说到给 Agent 配语音,我其实也试过其他方案,比如 OpenAI 的 TTS API,效果确实好,但每次调用都肉疼,而且在国内网络环境下延迟感人。微软的 edge-tts 算是免费方案里音质最接近真人的了。你们平时都在用什么 AI 工具?是像我一样自己折腾 Agent,还是直接用现成的语音助手?我在AI House 排行榜上看到不少新出的模型和工具,排名变化挺快的,没事儿上去刷两眼,给喜欢的模型投个票,也算支持一下开发者。你要是有什么好用的 TTS 方案或者踩坑经验,评论区聊聊,咱们一起把 Agent 调教得越来越像个人。
常见问题 / FAQ
先把话说出来:一个让我折腾到凌晨两点的需求
我有个 Hermes Agent,平时帮我整理邮件、查天气、管理日程,干得挺欢。但问题来了——它是个哑巴。每次我在地铁上,手机屏幕一亮,看到它给我推送的「您有 3 封新邮件,其中 2 封来自老板」,我还得掏出手机解锁、点开、看一遍。这体验太反人类了。我真正想要的是:它直接开口告诉我「老板发了封邮件,标题是催你交周报,截止时间是明天下午三点」。所以我决定给这个哑巴 Agent 装上声带,用 TTS...
第一步:装 edge-tts 并搞定第一个坑(依赖冲突)
先建个虚拟环境,别直接装全局,不然你的系统 Python 环境迟早被搞乱。我习惯用 venv,简单粗暴: cd ~/hermes-voice python3 -m venv venv source venv/bin/activate pip install edge-tts==6.1.9 这里我指定了版本号,因为 edge-tts 更新很频繁,6.2.0 之后的版本改了一些参数名,导致我的脚本报...
第二步:把 TTS 封装成 Hermes 的一个技能(技能目录结构)
Hermes Agent 的技能放在 ~/.hermes/skills/ 目录下,每个技能是一个文件夹,里面必须有一个 skill.yaml 文件描述元数据,还有一个 Python 文件实现逻辑。我建了个叫 tts_speak 的技能: mkdir -p ~/.hermes/skills/tts_speak cd ~/.hermes/skills/tts_speak touch skill.ya...
第三步:让 Hermes 认账(配置文件和触发测试)
技能文件写好了,需要告诉 Hermes 重新加载。Hermes 的配置文件在 ~/.hermes/config.yaml,你得检查一下有没有把技能目录正确指向。我打开配置文件看到默认的 skills_dir: ~/.hermes/skills,没问题。但有个细节:如果你改了技能文件夹的名字,必须重启 Hermes 服务,它不会热加载。我第一次改完没重启,直接跟 Agent 说「speak 你好」...
第四步:让 Agent 主动开口(跟记忆和定时任务联动)
光能被动响应还不够,我想要的是 Agent 主动播报。比如每天早上九点,它自动告诉我今天的日程安排。Hermes 有定时任务功能,用 hermes cron create 创建。我先写好一个返回文本的脚本,再把它跟 TTS 技能串起来。 创建定时任务的命令长这样: hermes cron create --name "morning_brief" --schedule "0 9 * * *" -...