Releases: iDoris-ai/AgentEar
Release list
v0.18.0 —— 边车内存可观测(且不报喜不报忧)+ 修两条测试 + 补上 CI 漏掉的 Python 闸
v0.18.0 —— 边车内存可观测(且不报喜不报忧)+ 修两条测试 + 补上 CI 漏掉的 Python 闸
三件事,代码改动都不大。
1. /health 多一个 mlx 块
active_mb / cache_mb / peak_mb / cache_limit_mb
cache_limit_set/cache_limit_api/cache_limit_error。
理由接 v0.17.0:ps 的 RSS 会被系统回收/压缩,看「有没有涨」得看 MLX 自己报的数。
实测(8bit,重启后):active 3072 / cache 0 / peak 3072,
cache_limit_set true、api "mx.set_cache_limit"。
cache_limit_mb: 256 当成事实:MLX 没有 get_cache_limit()
(实测 0.32.2:mx.get_cache_limit 与 mx.metal.get_cache_limit 都不存在),
所以那是我们想要的值,读不回来。「设上了没有」只看 cache_limit_set。
第一版直接把 256 当事实报出去——那属于报喜不报忧(调用失败时照样报 256),
本版改掉了。同一版还发现 mx.metal.set_cache_limit 已废弃
(0.32 会打 "will be removed… Use mx.set_cache_limit"),
所以优先调新名字,老名字只作旧版本退路——不然哪天老名字被删掉,闸会静默失效。
2. 修两条测试
- 方言 instruct 那条测试钉的是 v0.16.0 已经废掉的旧行为(断言语气描述和方言名
一起进 instruct)。方言档现在只写方言名,所以它必红。现在改成:语言档 → 语气 + 语系
都在;方言档 → instruct 恰好等于方言名、且不含语气描述,并扫DIALECT_STYLES
每条都走这个分支。这条测试是故意钉住「方言档不拼语气」的——看着像漏了语气,
实际是踩过坑(说了不像四川话)才砍掉的。 - 响度那条「RMS 差 10 倍」不是产品缺陷,是环境伪影:
normalize_loudness
在没有 numpy 时走ImportError分支原样返回,于是第一条报 10 倍差、
另两条假绿。整类加了 skip 守卫(实测确认:屏蔽 numpy 后复现的就是那个数)。
3. 补上 CI 里漏掉的那道闸
CI 原来只跑 build + clippy + cargo test,Python 边车测试根本不在里面。
后果是真的:v0.16.0 那条测试变红之后红着发布了两版没人知道。
现在加了 python3 -m unittest discover -s services/tts -p 'test_*.py'。
python3 没有 numpy,所以响度那 3 条在 CI 里是 skip
(本机三个解释器都有 numpy,正常应报 42→46 passed)。
skip 不等于通过。
数字
cargo test297 passed / 0 failed / 6 ignored(未变)- TTS 边车单测 42 → 46 passed
- 发布件:
AgentEar-0.18.0-macos-arm64.zip(257 MB)
已知边界(未变)
- V1 仍是打断式半双工,没有 VAD 自动打断、没有双讲,AEC 未解决
(误打断率与端到端打断延迟 <300 ms 仍未测)。 - 泰语在 MiniCPM5-2B 的能力边界外,实测不稳定(夹英文词 / 原样退回问句)。
- 4bit 与 bf16 的质量对比没做。
- 方言正不正宗只能人耳验收;那一栏只是开关,不能把普通话变成方言。
- ⛔ 职责边界(ADR-0008):命令执行 / 确认 UI / 回执展示归宿主 Agent24,
这条线上停止开发,等 6 个问题的答案。
v0.17.0 —— 修边车内存(无上限的 MLX 缓存)+ 一个启动即崩的 bug
v0.17.0 —— 修边车内存(无上限的 MLX 缓存)+ 一个启动即崩的 bug
你报「一个 voxcpm2 TTS 进程跑了一小时、占 38 GB,把我别的任务 OOM 掉了」。是我的锅。
先说老实话:我没复现出 38 GB
查的时候系统上已经没有任何 38 GB 的进程(最大的是你的 halmos 1.6 GB),
那个进程已经退出或被杀了。我实测到的是:
| 场景 | RSS |
|---|---|
| 边车空跑 1 小时 | 0.3 GB(自报峰值 3.4 GB) |
| 短文本 × 10 次 | 稳定 ~200 MB |
| 重负荷(长文本 + 98 秒长参考)× 8 次 | 3.35 → 3.48 GB 后走平 |
但缺陷是真的,而且正好能造成无上限增长。 不管当时那个数字是多少,这类问题都不该由你替我抓。
根因:我们从来没管过 MLX 的缓冲缓存
代码里没有任何 clear_cache,也没有缓存上限。MLX 释放张量时不把 Metal 缓冲还给系统,
只留在自己的池子里 —— 一个长期跑的服务反复合成之后 RSS 只增不减。
重负荷路径尤其明显:98 秒的参考每次都要重新编码 latent。
修法(两道闸):
mx.metal.set_cache_limit(256MB)—— 超了 MLX 自己回收;- 每次合成后
mx.clear_cache()(含异常路径)。
还补了可观测性:mlx_memory_mb()(active / cache / peak)。
ps 的 RSS 会被系统回收/压缩 —— 想看清增长必须看 MLX 自己报的数,
这也正是这次没能在第一时间抓到问题的原因。
顺带抓到一个更严重的 bug:边车会启动即崩
我把你的音色改名成「男声 / 女声」之后,启动脚本里写死的默认音色 female_zh_02 就不存在了
→ 走 else 分支 → echo "…$VOICE(…)"。
$VOICE 后面紧跟全角括号,bash 3.2 把多字节字符当成变量名的一部分
(VOICE\xef\xbc\x88),set -u 下直接 unbound variable 退出
→ 整个边车起不来,而日志里只有一行 shell 报错、看起来跟 TTS 毫无关系。
修法两条:
- 所有
$VAR紧跟非 ASCII 时一律加花括号(全文件扫过一遍); - 音色不在库里时兜底用库里第一条 —— 失败方向要选对:
宁可音色挑错(听得见、能改),也不能没声音(静默失败)。
你现在可以做的
- 我已经重启过边车,跑的是修好的版本(
女声兜底、缓存有上限)。 - 想随时确认:
curl -s localhost:8765/health看peak_rss_mb。 - 你的 halmos 任务被 OOM 这件事,我道歉 —— 那是我反复起边车、又加载了 16 GB 的
MOSS-TTS 测试造成的,跟你的任务无关。
验证
cargo test 297 passed(292 单测 + 5 集成);TTS 单测 40 passed。
对应 PR:#76 · 合并 commit 9891ce7
v0.16.0 —— 菜单拆成「语言 / 方言」,方言终于说对了
v0.16.0 —— 菜单拆成「语言 / 方言」,方言终于说对了
说话菜单现在是四栏
说话
├─ 音色 男声 / 女声 ← 你选的这两条
├─ 语言 普通话(默认)/ 英语 / 泰语
├─ 方言/口音 粤语 / 河南 / 四川 / 山东 / 东北 / 天津 / 英式 / 美式 / 加式
└─ 语气 亲切 / 平静 / 活泼 / 沉稳
默认普通话。 音色改名成 男声 / 女声(菜单里显示的就是文件名)。
改完下一轮立即生效,不用重启。
方言之前为什么不对
我写的方言指令是 (用四川话说,地道四川口音) —— 这正是官方文档警告的写法。
cookbook 的 Keep Instructions Simple 原话:
In the Control Instruction, simply type the dialect name (for example,
Cantonese).
Adding too many complex voice instructions might spoil the broth.
改成 (四川话) 三个字之后,四川话就出来了。
还有一条更重要的:方言靠正文,不靠这一栏。
同一个 (四川话) 指令下 |
结果 |
|---|---|
正文 幺儿,哈戳戳得你屋头来噶! |
✅ 四川腔 |
正文 今天天气不错,我们出去走一走。 |
❌ 还是普通话 |
官方 usage guide 的 Dialect tips 明说:write the target text in that dialect's own
vocabulary and expressions, not in standard Mandarin。
所以「方言」这一栏只是个开关,不能把普通话变成方言 —— 我不会把它写成「选了就会说四川话」。
实测(你验收过的)
边车真实链路、音色=男声:
| 方言 | 正文 | 出处 |
|---|---|---|
| 粤语 | 伙計,唔該一個A餐,凍奶茶少甜! | 文档原例 |
| 河南话 | 恁这是弄啥嘞?晌午吃啥饭? | 文档原例 |
| 东北话 | 你搁这整啥玩意儿呢? | 文档原例 |
| 四川话 | 幺儿,哈戳戳得你屋头来噶!… | 文档原例 + 我补的几句 |
| 山东话 / 天津话 | 我写的词 |
你的结论:「效果不错」。
顺带查到的两件事(都记进文档了)
① 非语言标记能治「机械感」。 官方 cookbook 的 Extra Spice 写着:
插入 [laughing] / [sigh] / [Uhm] / [Question-ah] 这类标记能让语音
「less mechanical」。你实测加了 [Uhm] 的比不加的好。
这条还没接进链路 —— 接法等你定。
② 「让 LLM 把回答改写成方言」这条路,用现在的模型走不通。
文档建议这么做,但我试了:本机那个 2B 直接把普通话原样退回来了(连指令都没跟)。
所以要真做「助手用方言回答」,得先换更大更听话的模型 —— 而换模型会把文字送到外部,
跟你这个项目的隐私前提冲突。这条我写进 CLAUDE.md 了,不要指望 2B 做方言改写。
cargo test 297 passed / 0 failed / 6 ignored;TTS 单测 40 passed。
对应 PR:#70 · 合并 commit 0baf064
v0.15.0 —— 修掉「克隆出来像机器人」的真凶
v0.15.0 —— 修掉「克隆出来像机器人」的真凶
一句话
声音像机器人不是参考音频的问题,也不是模型的问题 —— 是我的边车代码把参考音频喂错了采样率,
导致克隆输出整体高了一个八度。
真凶
load_audio(path) # mlx_audio 默认 sample_rate=24000 → 48k 的参考被重采样到 24k
ref_audio=<数组> # 返回的是裸数组,不带采样率;模型拿到【路径】时才会自己正确解码模型的 _encode_wav 拿到数组时只能自己假定一个速率 —— 于是参考被按错误速度解释。
实测(同一条 48 kHz 参考,F0 142.4 Hz):
| 输出 F0 | |
|---|---|
| 给路径(正确) | 147.7 / 133.7 / 150.5 Hz ✅ |
| 给数组(原来的做法) | 287.4 Hz ❌ 高了一倍 |
所以不管换成谁的参考都是同一个怪声 —— 16 kHz 的会话录音、48 kHz 的规范录音、还是模型自举的,
输出全落在 ~300 Hz 一带。听着像音色问题,实际是音高被抬了一个八度。
修法:让模型自己去解码(_load_ref 返回路径)。代价是每次合成重读一次参考 wav,可以忽略。
顺带查清的两条
instruct不是元凶:mlx-audio 确实会把它拼成(描述)正文(Voice Design 模式),
但带上 instruct + 正确路径时输出仍然跟住参考(133.7 Hz)。所以不用急着去掉它。- 官方文档的 Mode 5「Ultimate Cloning」(
ref_audio+prompt_audio+prompt_text,
同一段音频给两个参数,README 说"for maximum similarity")实测 150.5 Hz,是三种里最贴的。
以后要再提相似度,走这条。
音色与方言怎么用
菜单栏 → 说话 → 三栏:
| 选项 | |
|---|---|
| 语系 | 普通话 / 粤语 / 河南 / 四川 / 山东 / 东北 / 天津 / 英式 / 美式 / 加式 / 泰 |
| 语气 | 亲切 / 平静 / 活泼 / 沉稳 |
| 音色 | 扫目录 —— 文件名就是菜单里显示的名字 |
- 音色已经收拾成两条:男声(142.4 Hz)与 女声;其余归档到
voices/archive/(菜单里不显示)。 - 音色 × 语系任意组合:参考决定音色,指令决定口音/语气(官方叫 Controllable Voice Cloning)。
实测「男声 + 四川话」音色仍然跟住参考(153.8 vs 144.4 Hz)。 ⚠️ 方言说得地不地道只能人耳验收。声学指标只能证明「发音变了、音色没被带跑」,
证明不了四川话正宗 —— 这条我没法替你判断。
这次的教训(写给以后)
先拿「直接调模型」当基准,把变量隔离出来。 我在确认这一点之前,连换了三轮参考音频
(16 kHz 会话录音 → 48 kHz 规范录音 → 模型自举),方向全错。
症状("机器人声")离病因(采样率)太远,靠猜是猜不到的。
验证
cargo test 292 + 5、TTS 单测 40 passed;bundle 冒烟测试通过。
v0.14.0 —— 边界固化:AgentEar 只做语音,执行交给 Agent24
v0.14.0 —— 边界固化:AgentEar 只做语音,执行交给 Agent24
一句话
AgentEar 负责「听见」和「说出」;谁去执行、怎么确认、写到哪、回执怎么展示,
是宿主(Agent24)的事。 AgentEar 只提出动作,不执行它。
冻结的接口:agentear.proposal/1
agentear --match-command "发邮件给 a@b.com 讨论 AEC" --json{
"schema": "agentear.proposal/1",
"matched": "发邮件给",
"rest": "a@b.com 讨论 AEC",
"action": { "type": "open_url", "url": "mailto:{rest}" },
"needs_confirm": true,
"prompt": "要打开邮件草稿:a@b.com 讨论 AEC。确认就按一下键,或者说「确认」。"
}这个入口绝不执行任何东西——它只回答「这句话是什么意思、要不要确认、对象是谁」。
确认界面、执行、凭据、回执展示全在宿主那侧。
(prompt 只在 needs_confirm 为真时存在:本地动作不该拿到
「要执行 style」这种内部名字去显示。)
逻辑验证:假宿主
scripts/agent24-standin.py 把宿主那一侧的最小骨架跑起来了:
① 问 AgentEar 拿 proposal → ② 自己显示 → ③ 自己确认 → ④ 自己执行 → ⑤ 自己展示回执
它故意不调用 --run-command——用了就等于把执行又塞回 AgentEar。
文档
docs/decisions/0008-voice-frontend-boundary.md:职责切分表、
接口字段表、对 Agent24 的 6 条诉求、以及「明确不做」的清单。
那 6 条诉求是这一版最需要回答的部分(尤其是第 2 条):
- 事件通道:子进程 JSONL / 本地 HTTP / WebSocket —— 要选一个
- 热键归谁 —— 两个 app 都装 CGEventTap 会都收到右 Command,
而「谁负责录音」必须只有一个 - 麦克风与播放的归属(打断语义依赖「谁记住正在播什么」)
- 配置归属(
~/.agentear/还要不要是唯一真相) - TCC 权限是 per-app 的(音频搬过去要重新授权一次)
- 回执留档归谁(现在只打日志,没落库)
⛔ 在这一版之后,这条线上停止开发
不再新做:初始化向导、凭据管理、回执界面、历史列表、多轮追问。
已经做出来的保留,AgentEar 单独跑时仍然有用:
- 语音二次确认(v0.12.0)
- 动作回执的读取与如实报错(v0.13.0)
音色(顺带,供参考)
用你自己的一段录音(今天测试留下的 49 秒)生成了一条参考音色 jason_zh_01,
和默认的 female_zh_02 做了 A/B。两条都在 ~/.agentear/talk/voices/,
菜单里能切。
「真人参考比自举参考更像人」,不是最终音色。
cargo test 297 passed / 0 failed / 6 ignored;clippy 7 条。
对应 PR:#66 · 合并 commit 009935b
v0.13.0 —— 动作回执要真的回执(不再谎报成功)
v0.13.0 —— 动作回执要真的回执(不再谎报成功)
起因是测试里那句「你别骗我啊」。查下去发现,写外部那一步有两处报喜不报忧,
而且都精确对应被问到的问题。
修掉的两处谎报
① 响应体被丢掉了 —— Notion / n8n 写入成功后回的正是新页面的 URL。
所以问「你写到哪了?把网址给我看看」时,系统手里根本没有那个答案——
不是不给,是从来没接住。
② 退出码不看 —— HTTP 401(token 过期)/ 500 也照样报「已发送到 …」。
没写进去却说写进去了,比失败更糟:你会以为成了,然后去 Notion 里找一个不存在的页面。
现在:
| 场景 | 现在会告诉你 |
|---|---|
| 成功 | ⚡ 已发送到 …;返回:https://www.notion.so/abc123def |
| 失败(401) | 没写进去(curl 退出码 Some(22)):… 401 / {"message":"API token is invalid"} |
(旧版这两条都报「已发送到 …」。)
顺带把 curl -f 换成 --fail-with-body:-f 在 HTTP 出错时一个字都不输出,
于是服务端那句「哪里配错了」被吞掉——而配置阶段最需要的正是那一句。
另外补了一条:模型说了什么,现在进日志了
流式改造(v0.10.0)时,回答正文被丢出日志,只剩「回答 N 字」。
实测就是这里卡住的:你复述模型说过「我无法访问外部链接」,
而我们从日志里读不出这句话,只能重新打一遍模型才知道。
这次测试的三个事实(原样记下)
- 那次没有写入任何地方,也不存在什么网址。 日志里指令表 0 次命中——
你说的话一句都没匹配上(默认短语是「发邮件给」,你说的是「你能帮我发个邮件吗」;
「no神」是 ASR 把 Notion 听错了)。全部当成闲聊发给了 2B 模型,它只回了一句话。 - 模型说「无法访问外部链接」不是权限设置,是它自己的套话模板。
拿同样的系统提示词重打一遍,它回:「我的知识截止于2023年10月,且无法访问外部链接」。
LLM 那条路上根本没有工具——我们从来没给它任何行动能力。 - 回执目前只印在 stdout / 日志里,没有念出来(念一串 URL 也不好听)。
仍未做
- Notion / 邮件的初始化:账号、凭据、写到哪个库、发给谁——一条都没有。
现在只能手写commands.json。这是下一个要做的。 - 播放音乐:没实现(不是「不需要配置」,是没做)。
- 音色:两个默认参考音频都是模型自举生成的,所以听起来必然是合成的。
- AEC 未解决;推键式打断;
mailto:的执行分支没实跑验证过。
cargo test 297 passed / 0 failed / 6 ignored;clippy 7 条(与基线持平)。
对应 PR:#65 · 合并 commit 2d1020e
v0.12.1 —— 向外动作二次确认(+ 挑解释器的版本闸)
v0.12.1 —— 向外动作二次确认(+ 挑解释器的版本闸)
功能与 v0.12.0 相同。 这一版只多了两件事:一个脚本修复,和一个让版本号对得上的 bump。
1. setup-talk.sh 挑 Python 解释器的真问题(脚本修复)
原来它只看命令名存不存在:
for candidate in python3.12 python3.13 python3.14 python3.11; do
command -v "$candidate" >/dev/null && { PY="$candidate"; break; }
done两个毛病:
python3.14排在 3.11 前面会被选中——而 mlx 的 wheel 覆盖到哪一版我们没验证过,
名字存在不等于能装上。python3不在候选里,所以在「只有 pyenv、没有python3.11这个别名」的机器上,
脚本会报**「找不到 Python 3.11+」——明明有可用的**。
现在每个候选都真的问一次版本(sys.version_info >= (3, 11))才算数,
不合格的打一行日志跳过;python3 也进候选。
⚠️ 顺序没有改(3.12 仍优先)。没有证据说 3.14 装不了 mlx,按「不确定就不动」处理,
只在注释里标明 3.14 未验证。改了顺序的话,venv 建在哪个版本上就取决于
是哪个 shell 跑的 setup(交互式有 pyenv 的 3.11、非交互没有),那是更难查的不确定性。
2. 顺便记下一件容易误会的事
「默认 Python 是 3.11.9」只对交互式 shell 成立:
| 环境 | python3 |
python3.11 |
|---|---|---|
| 你的终端(交互式 zsh) | pyenv 3.11.9 | ✅ 有 |
| 非交互 / launchd / GUI 启动的进程 / CI | /usr/bin/python3 = Xcode 3.9.6 |
❌ 找不到 |
.zshrc 在非交互下不加载,pyenv 就不在 PATH 里。3.9.6 连 import mlx_lm 都做不到
(mlx 要 3.11+)。也就是说"这台机器的默认 Python"取决于谁在问。
现状没踩到:两个边车脚本都钉 $VENV/bin/python,而那个 venv 是
uv 的 cpython 3.11.15——与 pyenv 无关。所以边车实际跑在 3.11.15 上,
pyenv 那份 3.11.9 一次都没被用到。
3. 为什么有版本号
版本号在二进制里(src/tray.rs:AgentEar {CARGO_PKG_VERSION}),
所以 bump 之后打出来的 .app 与 v0.12.0 不是同一个字节流。
- 如果你没在用 v0.12.0:装这个。
- 如果你已经在用 v0.12.0:不必重装——功能一模一样,
这次唯一的实质改动在scripts/(不在 .app 包里)。clone 仓库的人直接拿到。
验证
cargo test296 passed / 0 failed / 6 ignored(291 单测 + 5 集成)- 版本闸实测拦得住 3.9.6;两种 shell 各跑一遍选择逻辑,都落在 3.11+
- bundle 冒烟测试通过
仍未解决(与上一版一样,别读成做过了)
- AEC 未解决;推键式打断,不是全双工
- 音色未经人耳验收;8bit 不是"音质档"
mailto:的执行分支没实跑验证过(会打开邮件客户端)- 确认逻辑是启发式(长度上限 + 否定前缀),不是理解
v0.12.0 —— 向外动作必须二次确认
v0.12.0 —— 向外动作必须二次确认
会把内容送出去的动作,执行前一定先问一句。
写 Notion、发邮件这类动作有两个性质:① 出了这台机器 ② 大多不可撤。
而触发它们的是语音识别——一个会听错的东西。听错一次描述,代价是重说一遍;
听错一次写出去,代价是别人收到了错的东西,而且没有撤销键。
| 动作 | 会不会问 |
|---|---|
写 Notion / 调 webhook(http_post) |
✅ 一定问 |
发邮件(mailto:) |
✅ 一定问 |
| 打开网页 / 搜索 | ❌ 默认不问(单条指令可加 "confirm": true) |
| 切音色 / 切模式 / 记笔记 | ❌ 在本机、可逆 |
为什么搜索不问:每次都问的确认会被按成肌肉记忆——那等于没有确认,
还把高频动作慢了一倍。
确认长什么样
它把内容念给你听,而不是只问「确认吗」——你没法确认一个没被告知的东西:
你:记到notion 明天要测 AEC
它:要把这条发到 http://…/notion,内容是「{"title":"明天要测 AEC"}」。
确认就按一下键,或者说「确认」。 ← 此刻什么都没发出去
你:(按一下录音键) 或 说「确认」
它:⚡ 已发送到 http://…/notion
- 按一下录音键 = 确认(不用说话);说「确认 / 对 / 好的」 = 确认。
- 说「取消 / 不用 / 别发」 = 作废;说别的话 = 也作废,并把这句当新的一轮。
- 不吭声、等 30 秒 = 自动作废(
command_confirm_secs可改,最短 5 秒)。 - 菜单栏显示
❓—— 待确认在界面上必须看得见,否则你只会觉得「说了没反应」。
几条刻意做严的地方
- 念出来的内容 == 将要发出去的内容(同一份文本,不各算一遍)。
念 A 发 B 的话,这个「二次确认」就只是让你点了个头。 - 否定先判:「不确认」「不要发」里含着肯定词——先判肯定就会把
「别发」执行成「发」。这是唯一会真正出事的方向。 - 同意只在短答复里认(≤7 字):「我刚才确认过了吗」这种提到确认的长句
不算同意。而拒绝不设长度限制——这个不对称是有意的:
误判成拒绝只是让你再说一遍,误判成同意就发出去了。 - 同时只留一条待确认:挂两条时你说「确认」,它不知道该确认哪条,
而猜错就是把错的东西发出去。
自己验一下(不用麦克风)
agentear --run-command "记到notion 明天要测 AEC" # 会问,不执行
agentear --run-command "记到notion 明天要测 AEC" --reply 确认 # 两轮:先问后答,才发
agentear --match-command "发邮件给 a@b.com" # 干跑,顺带报要不要确认本版实测(真起了一个 HTTP webhook 逐条验):只有「确认」和「好的,发吧」会发出去,
且发出去的 payload 与念出来的完全一致;「别发 / 取消 / 今天天气怎么样 /
我刚才确认过了吗」四条一条都没发。
⚠️ mailto:的执行分支没有实跑验证过(跑它会在你机器上打开邮件客户端)。
已验证的是「它会问」以及 http_post 的完整链路。
还没做
- 确认是启发式(长度上限 + 否定前缀),不是理解。要更稳得上 LLM 判意图。
- 没有撤销:发出去就发出去了。这版只保证「发之前问过」。
- 通话的其他边界一条未变:推键式打断、AEC 未解决、不是全双工、
音色未经人耳验收。
cargo test 291 单测 + 4 集成 = 295 passed / 0 failed / 6 ignored;clippy 7 条。
安装
下载 AgentEar-0.12.0-macos-arm64.zip,解压后把 AgentEar.app 拖进「应用程序」。
要求 Apple Silicon Mac(M1 及以上)。首次启动要各授权一次麦克风与
辅助功能(TCC 权限不会从终端带到 .app,两者是独立主体)。
对话模式要两个本地边车(模型不随包分发):先跑一次 scripts/setup-talk.sh。
对应 PR:#61 · 合并 commit 6ab82dc
v0.11.0 —— 量化档可选(默认 4bit)+ 开箱默认音色库
v0.11.0 —— 量化档可选(默认 4bit)+ 开箱就有一个固定音色
一、量化档:默认 4bit,内存宽裕可以要 8bit
| 权重 | 边车进程峰值 RSS | |
|---|---|---|
| 4bit(默认) | 2.30 GB | 约 2.4–2.5 GB |
| 8bit(可选) | 3.22 GB | 约 3.3 GB |
scripts/setup-talk.sh # 默认 4bit
scripts/setup-talk.sh --tts-quant 8bit # 要 8bit 就显式说
AGENTEAR_TTS_QUANT=8bit scripts/serve-tts.sh # 启边车时也指同一档默认档只能是 4bit —— 发布的机器内存可能没这么大。这条有测试钉住
(tests/script_defaults.rs),谁改默认档 CI 会红。
⚠️ 8bit 目前买不到可测的音质:实测没有检出两档的输出质量差异。
花掉的那 1 GB 内存换来的是「更大」。想改善音色请换参考音频
(services/tts/make_voice.py),不是换量化档。别把 8bit 读成「音质档」。
顺带纠正一个数字
8bit 的权重是 3.22 GB,不是 2.3 GB —— 2.3 GB 是 4bit 的文件大小。
上一版把这两个记反了。已在文档、脚本注释与本说明里改正。
二、开箱就有一个固定音色(这条修的是一个真 bug)
VoxCPM2 是零样本克隆:不给参考音频,每次合成都会随机换一个说话人
(边车自己的告警:实测 F0 极差 65%、音量差 4.5 倍)。
而启动脚本从来没有把音色库传给边车,所以:
- 只有「边车由 AgentEar 按配置拉起」这条路径会中招;
手工带音色库参数起的那条是好的 —— 两套起法行为不同,这就是它一直没被发现的原因。 - v0.10.0 的句子级流水线又把它降了一级:一次回答切成好几句、每句各发一次请求,
于是不只是「这次和上次不像」,而是同一句话里换好几个人在说。
现在:仓库里带两条实测挑过的参考音频(assets/talk-voices/,1.9 MB,
VoxCPM2 自己生成的,不是真人录音),装好后边车默认钉住 female_zh_02。
- 你已有的音色库不会被覆盖——要换就自己造一条。
- 数据目录里还没有时,回退用仓库里那份(clone 出来就有,不用下载)。
⚠️ 这次修的是「没有参考音频」这一条,不是「音色好不好」这一条。
音色能不能接受仍然只能由人耳判定——声学代理指标区分不出配置。
如果听着还是不满意,下一步该动的是参考音频,不是量化档。
三、其他
cargo test281 → 285 passed / 0 failed / 6 ignored(新增 4 条钉脚本默认值的集成测试)- clippy 7 条(与基线持平)
还没做
- 量化档没有菜单入口:切档要改
talk_tts_start_command。做菜单项就得先有
「另一档还没下载」的按需下载 UI。 - 通话的其他边界一条未变:推键式打断、AEC 未解决、不是全双工、
音色未经人耳验收。
安装
下载 AgentEar-0.11.0-macos-arm64.zip,解压后把 AgentEar.app 拖进「应用程序」。
要求 Apple Silicon Mac(M1 及以上)。首次启动要各授权一次麦克风与
辅助功能(TCC 权限不会从终端带到 .app,两者是独立主体)。
对话模式要两个本地边车(模型不随包分发):先跑一次 scripts/setup-talk.sh。
对应 PR:#60 · 合并 commit 12cb239
v0.10.0 —— 句子级流水线:边出边合成
v0.10.0 —— 句子级流水线:边出边合成
你问完到听见第一个字,中位数从 4.86 秒压到 2.80 秒。
老路径是三拍串行:等模型把话说完(0.35–0.85s)→ 整段送去合成(2.4–6.5s)
→ 才开始播。现在改成出一句就合成一句,合成一段播一段——
你在听上一句的时候,下一句已经在合成了。
实测(M1 Max / 64 GB,同一句提问交替各跑若干次)
| n | 最小 | 中位数 | 最大 | |
|---|---|---|---|---|
| 整句路径(旧) | 8 | 3.27 s | 4.86 s | 6.94 s |
| 流式路径(新,默认) | 11 | 2.27 s | 2.80 s | 5.55 s |
⚠️ 两个分布是重叠的:流式最差的一次(5.55 s)比整句最好的一次(3.27 s)还慢。
凭单次结果说「流式一定快」是错的——所以这里给中位数与极值,不给一个好看的区间。- 而且整句路径随回答长度增长、流式不随,所以回答越长差距越明显。
- 句间没有可测的空隙:一次实测整轮墙钟 10.40 s(首字 2.27 + 播出 7.85)。
- 明细见 benchmarks-talk.md §2.5。
这版说明最初只采了 5 次流式样本(2.27–2.78 s),那一批恰好落在好的一侧;
多采几次就冒出 3.31 / 4.49 / 5.55。这是「同配置复测方差大于配置间差异」的
第二次实例(第一次是音色那轮的半音数),所以数字已按中位数重写。
边界要说清(这几条比上面的数字更重要)
- 一轮的总时长基本没变。 省的是等待,不是计算。
别把「首字变快」读成「一轮变快」。 - 地板是 TTS 那一次合成(约 2.4s 固定开销,实测 2.4–6.5s 方差很大),不是 LLM(这里只占 0.35–0.78s)。
所以不要写成「压到了 1 秒」 —— 想再往下压,要压的是 TTS,不是 LLM。 - TTS 侧没有流式可给(
mlx_audio对短句只产出一段),所以这不是「音频流式」,
是把两段等待重叠起来。 - 想退回老路径:
agentear --ask "…" --no-stream。
这一版踩到两个「静默」坑,都写进了代码注释
- 流式请求的 URL 漏拼路径 → 404 → 被上层当成「不支持流式」静默退回整句路径。
- 退回整句路径时忘了把正文交给下游 → 切句、合成、播放全都不启动:
有文字、没声音,而日志里一切正常。
教训是同一条:「退回老路」的分支必须有输出,否则它比慢得多更糟——
你看到文字出来了,却不知道为什么没声音。这两条都是把 A/B 真跑起来才发现的,
只跑单元测试发现不了。
其他
- LLM 侧走 SSE 流式(
stream: true),用curl -N逐行读;thread::scope借回调,
没有引入unsafe。 - 切句判据:句末标点 + 至少 6 个字 + 标点不是当前缓冲区的最后一个字符。
最后这条是「按标点随手切」最容易翻车的地方:3.切下去,5 度就变成独立一句。
版本号v0.9.0也靠这条排掉。 - 未闭合的思考段一个字都不放(否则会听见模型的内心独白)。
⚠️ 这一条与一次性路径的规则不同(那条是不吞)。差别有理由,但不要读成已经统一。 cargo test268 → 281 passed / 0 failed / 6 ignored;clippy 7 条(比基线少 1 条)。
还没做
- TTS 那约 2.4s 的固定开销没压 —— 这是下一个数量级的关键,得动边车那一侧。
- 打断延迟仍未测(与 T3.4.2 同一条判据)。
- 通话的其他边界一条未变:推键式打断、AEC 未解决、不是全双工;
音色仍未经人耳验收(见 v0.8.1 说明)。
安装
下载 AgentEar-0.10.0-macos-arm64.zip,解压后把 AgentEar.app 拖进「应用程序」。
要求 Apple Silicon Mac(M1 及以上)。首次启动要各授权一次麦克风与
辅助功能(TCC 权限不会从终端带到 .app,两者是独立主体)。
对话模式要两个本地边车(模型不随包分发):先跑一次 scripts/setup-talk.sh。
对应 PR:#58 · 合并 commit 30135ba