Skip to content

Releases: iDoris-ai/AgentEar

v0.18.0 —— 边车内存可观测(且不报喜不报忧)+ 修两条测试 + 补上 CI 漏掉的 Python 闸

Choose a tag to compare

@jhfnetboy jhfnetboy released this 16 Sep 06:31
6a46516

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 trueapi "mx.set_cache_limit"

⚠️ 别把 cache_limit_mb: 256 当成事实:MLX 没有 get_cache_limit()
(实测 0.32.2:mx.get_cache_limitmx.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'

⚠️ CI 的 python3 没有 numpy,所以响度那 3 条在 CI 里是 skip
(本机三个解释器都有 numpy,正常应报 42→46 passed)。
skip 不等于通过。

数字

  • cargo test 297 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

Choose a tag to compare

@jhfnetboy jhfnetboy released this 16 Sep 06:10
9891ce7

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()(含异常路径)。

⚠️ 256 MB 是折中:留一点够复用、又不会无限长。设成 0 反而更慢(每次重新分配)。

还补了可观测性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 毫无关系。

修法两条:

  1. 所有 $VAR 紧跟非 ASCII 时一律加花括号(全文件扫过一遍);
  2. 音色不在库里时兜底用库里第一条 —— 失败方向要选对:
    宁可音色挑错(听得见、能改),也不能没声音(静默失败)

你现在可以做的

  • 已经重启过边车,跑的是修好的版本(女声 兜底、缓存有上限)。
  • 想随时确认:curl -s localhost:8765/healthpeak_rss_mb
  • 你的 halmos 任务被 OOM 这件事,我道歉 —— 那是我反复起边车、又加载了 16 GB 的
    MOSS-TTS 测试造成的,跟你的任务无关。

验证

cargo test 297 passed(292 单测 + 5 集成);TTS 单测 40 passed。


对应 PR#76 · 合并 commit 9891ce7

v0.16.0 —— 菜单拆成「语言 / 方言」,方言终于说对了

Choose a tag to compare

@jhfnetboy jhfnetboy released this 15 Sep 11:41
0baf064

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 —— 修掉「克隆出来像机器人」的真凶

Choose a tag to compare

@jhfnetboy jhfnetboy released this 15 Sep 11:15
37b5872

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 冒烟测试通过。

⚠️ 仍然没有做人耳确认 —— 如果你听着还是不对,直说。


对应 PR#69(发版)·
修复来自 #68 · 合并 commit 37b5872

v0.14.0 —— 边界固化:AgentEar 只做语音,执行交给 Agent24

Choose a tag to compare

@jhfnetboy jhfnetboy released this 15 Sep 02:34
009935b

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 条):

  1. 事件通道:子进程 JSONL / 本地 HTTP / WebSocket —— 要选一个
  2. 热键归谁 —— 两个 app 都装 CGEventTap 会都收到右 Command
    而「谁负责录音」必须只有一个
  3. 麦克风与播放的归属(打断语义依赖「谁记住正在播什么」)
  4. 配置归属(~/.agentear/ 还要不要是唯一真相)
  5. TCC 权限是 per-app 的(音频搬过去要重新授权一次)
  6. 回执留档归谁(现在只打日志,没落库)

⛔ 在这一版之后,这条线上停止开发

不再新做:初始化向导、凭据管理、回执界面、历史列表、多轮追问

已经做出来的保留,AgentEar 单独跑时仍然有用:

  • 语音二次确认(v0.12.0)
  • 动作回执的读取与如实报错(v0.13.0)

音色(顺带,供参考)

用你自己的一段录音(今天测试留下的 49 秒)生成了一条参考音色 jason_zh_01
和默认的 female_zh_02 做了 A/B。两条都在 ~/.agentear/talk/voices/
菜单里能切。⚠️ 源录音只有 16 kHz、估算 SNR ≈ 21.6 dB,所以它验证的是
「真人参考比自举参考更像人」,不是最终音色

cargo test 297 passed / 0 failed / 6 ignored;clippy 7 条。


对应 PR#66 · 合并 commit 009935b

v0.13.0 —— 动作回执要真的回执(不再谎报成功)

Choose a tag to compare

@jhfnetboy jhfnetboy released this 15 Sep 02:21
2d1020e

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 字」。
实测就是这里卡住的:你复述模型说过「我无法访问外部链接」,
而我们从日志里读不出这句话,只能重新打一遍模型才知道。

这次测试的三个事实(原样记下)

  1. 那次没有写入任何地方,也不存在什么网址。 日志里指令表 0 次命中——
    你说的话一句都没匹配上(默认短语是「发邮件给」,你说的是「你能帮我发个邮件吗」;
    「no神」是 ASR 把 Notion 听错了)。全部当成闲聊发给了 2B 模型,它只回了一句话。
  2. 模型说「无法访问外部链接」不是权限设置,是它自己的套话模板。
    拿同样的系统提示词重打一遍,它回:「我的知识截止于2023年10月,且无法访问外部链接」。
    LLM 那条路上根本没有工具——我们从来没给它任何行动能力。
  3. 回执目前只印在 stdout / 日志里,没有念出来(念一串 URL 也不好听)。

仍未做

  • Notion / 邮件的初始化:账号、凭据、写到哪个库、发给谁——一条都没有。
    现在只能手写 commands.json。这是下一个要做的。
  • 播放音乐:没实现(不是「不需要配置」,是没做)。
  • 音色:两个默认参考音频都是模型自举生成的,所以听起来必然是合成的。
  • AEC 未解决;推键式打断;mailto: 的执行分支没实跑验证过。

cargo test 297 passed / 0 failed / 6 ignored;clippy 7 条(与基线持平)。


对应 PR#65 · 合并 commit 2d1020e

v0.12.1 —— 向外动作二次确认(+ 挑解释器的版本闸)

Choose a tag to compare

@jhfnetboy jhfnetboy released this 15 Sep 02:04
4ba00d8

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.rsAgentEar {CARGO_PKG_VERSION}),
所以 bump 之后打出来的 .app 与 v0.12.0 不是同一个字节流

  • 如果你没在用 v0.12.0:装这个。
  • 如果你已经在用 v0.12.0不必重装——功能一模一样,
    这次唯一的实质改动在 scripts/(不在 .app 包里)。clone 仓库的人直接拿到。

验证

  • cargo test 296 passed / 0 failed / 6 ignored(291 单测 + 5 集成)
  • 版本闸实测拦得住 3.9.6;两种 shell 各跑一遍选择逻辑,都落在 3.11+
  • bundle 冒烟测试通过

仍未解决(与上一版一样,别读成做过了)

  • AEC 未解决;推键式打断,不是全双工
  • 音色未经人耳验收;8bit 不是"音质档"
  • mailto:执行分支没实跑验证过(会打开邮件客户端)
  • 确认逻辑是启发式(长度上限 + 否定前缀),不是理解

对应 PR#64(版本 bump)·
修复来自 #63 ·
合并 commit 4ba00d8

v0.12.0 —— 向外动作必须二次确认

Choose a tag to compare

@jhfnetboy jhfnetboy released this 15 Sep 01:40
6ab82dc

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)+ 开箱默认音色库

Choose a tag to compare

@jhfnetboy jhfnetboy released this 15 Sep 01:18
12cb239

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 test 281 → 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 —— 句子级流水线:边出边合成

Choose a tag to compare

@jhfnetboy jhfnetboy released this 14 Sep 15:57
30135ba

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

这一版踩到两个「静默」坑,都写进了代码注释

  1. 流式请求的 URL 漏拼路径 → 404 → 被上层当成「不支持流式」静默退回整句路径。
  2. 退回整句路径时忘了把正文交给下游 → 切句、合成、播放全都不启动:
    有文字、没声音,而日志里一切正常。

教训是同一条:「退回老路」的分支必须有输出,否则它比慢得多更糟——
你看到文字出来了,却不知道为什么没声音。这两条都是把 A/B 真跑起来才发现的,
只跑单元测试发现不了

其他

  • LLM 侧走 SSE 流式(stream: true),用 curl -N 逐行读;thread::scope 借回调,
    没有引入 unsafe
  • 切句判据:句末标点 + 至少 6 个字 + 标点不是当前缓冲区的最后一个字符
    最后这条是「按标点随手切」最容易翻车的地方:3. 切下去,5 度 就变成独立一句。
    版本号 v0.9.0 也靠这条排掉。
  • 未闭合的思考段一个字都不放(否则会听见模型的内心独白)。
    ⚠️ 这一条与一次性路径的规则不同(那条是不吞)。差别有理由,但不要读成已经统一
  • cargo test 268 → 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