三天写了一个本地 AI 女友:149 个提交,75 个在修 bug,最贵的教训我犯了三次

Image

三天,149 个提交。其中 75 个 fix,40 个 feat——修 bug 的次数几乎是加功能的两倍。

做的东西叫 Lumi:一个待在 macOS 桌面角落的 AI 女友,会说话、听得见你说话、脸上有表情,全程不联网。语音识别、大模型、语音合成,三样都在这台机器上跑。

先说清楚一件事,不然后面很多话会读不懂:这些代码不是我敲的,是 Claude Code 写的。

我干的是另外三件事:提需求、听声音、拍板。它写代码、跑基准、读元数据、写测试;我说”慢”、说”这是男声”、说”5 秒无法忍受”。它交付,我验收。

这篇不是教程。教程网上一搜一大把,架构图谁都画得出来。我想记的是那些它以为对、我也跟着信了、结果完全错的判断——尤其是其中一个,一模一样地来了三次:它写完,测试全绿,跟我说修好了;我签收;然后我一看,还是那样。

三次都是我签收的。那三次,是这三天里最贵的东西,比代码贵。


一、先说这东西是什么

一句话链路:

麦克风 → 能量 VAD → MLX Whisper → Ollama → MLX Qwen3-TTS → 扬声器

分三个进程:

进程 干什么 技术栈
桌面窗口 透明立绘、口型、表情 Tauri 2 + PixiJS
语音守护进程 整条语音链路 Python 3.12 + MLX
Ollama 大模型 本地 11434 端口

为什么要拆成两个进程? 因为模型加载要十几秒。守护进程常驻、模型保持热态,窗口只是个瘦客户端,关了再开不用重新等。

窗口和守护进程之间:HTTP 控制接口(8767 端口)+ WebSocket 推状态,带 bearer token 鉴权,全部绑定 loopback。

机器是 M4 / 48G 内存。就这么点家当。


二、以为对、结果全错的五件事

1. 一个性能测试证明了”LLM 不是瓶颈”,可那个模型根本没输出

我说慢。它第一反应是模型太大(gpt-oss:20b,14GB),跑了个基准:

gpt-oss:20b 53.8 tok/s
qwen2.5:7b 50.8 tok/s

数字摆在这儿,它得出结论:LLM 不是瓶颈,去查 TTS。我看了那两行数字,也点头了。然后是大半天的语音合成研究,换了另一套引擎,下了 9GB 模型。

后来打开日志,第一行就写着:

[ollama] empty content model=gpt-oss:20b reason=length eval=256
[native] ASSISTANT: (empty)

那个模型每轮花 5.8 秒,产出一个空字符串。

gpt-oss 是推理模型,先生成一大段隐藏思考。256 token 的预算全烧在思考上了,done_reason=length 的时候正文一个字还没开始写。

那个基准测的是 tokens/秒,压根没看输出内容是不是空的。数字很好看,结果是空的。而我,看着一张很好看的表点了头。

换成 qwen2.5:7b(非推理模型),一句话解决:

qwen2.5:7b → “听到了。” done_reason=stop 思考浪费 0 字
gpt-oss:20b → “” done_reason=length 思考浪费 272 字

教训:跑基准之前,先确认被测的那个东西真的产出了东西。快而空,是最容易骗过自己的一种”性能”。

顺手加了道防线——空回复不再闷声不响,直接把模型选错这件事说出来:

if reason == “length” and reasoning:
print(f”‘{model}’ is a reasoning model and spent its whole “
“budget thinking. Set a non-reasoning model.”)

2. 她开始自言自语,因为她成了孤儿

我报了个很邪门的现象:关掉 app 之后她还在说话,但听不见我说话了。

查进程:

$ ps -o pid=,ppid= -p 10761
10761 1

ppid=1。守护进程被 launchd 收养了——窗口进程死了,没把它带走。它继续开着麦克风,听见自己音箱里的声音,把自己的话当成用户输入,然后回答自己。

一个人在那儿自问自答,没完没了。

第一次修:在窗口的退出处理里发关闭请求。没用——退出时零输出,连报错分支都没走到。而且这个思路本身就是错的:崩溃、强制退出、SIGKILL,这些情况下窗口根本没机会说任何话。

第二次才换对了思路——让守护进程自己盯着父进程

def is_orphaned(*, original_parent: int, current_parent: int) -> bool:
if original_parent <= 1:
return False # 本来就没父进程(终端启动),不管
return current_parent != original_parent

父进程一死,内核一定会把孤儿过继出去,轮询 getppid() 能捕获所有情况。验证方式也简单粗暴:kill -9 强杀窗口,守护进程自己干干净净地退了。

教训:需要对方配合才能生效的清理逻辑,在对方”死得不体面”的时候一定失效。让被依赖的那一方,自己负责自己的生死。

3. 方言:能做,但代价是 5 秒

想让几个角色说不同方言。查下来两条路。

Qwen3-TTS 自带方言音色,模型元数据里写得明明白白:

eric → sichuan_dialect
dylan → beijing_dialect

速度极好,RTF 0.27(比实时快 3.7 倍)。它把这段写得很兴奋,我看着也高兴。

然后我戴上耳机听了一句:男声。

元数据里有 spk_is_dialect没有性别字段。名字叫 Eric、Dylan,它本该想到的——可它没有耳朵,它听不了音频。角色全是女生,男声直接把人设毁了。

这是整个项目里第一次,我意识到自己在这条流水线上是干什么的。

只剩 CosyVoice3 这一条路(能克隆女声、支持粤语闽南话)。接进去了,跨进程桥接也写了(它要 Python 3.10,主项目是 3.12),结果:

引擎 RTF
Qwen3-TTS 0.24 ~ 0.35
CosyVoice3 1.6 ~ 2.9

慢 6 到 10 倍。我听完一句话定论:“5 秒无法忍受。”

全部删掉——后端、worker、路由器、配置项,四个文件。

教训:技术上可行 ≠ 产品上可用。大半天做了个最终被删掉的功能,就因为动手前没人先问一句”慢多少你能接受”。这句话只有我问得出来,而我问晚了。

顺带一提,中间它还搜了一圈”全网有没有又快又能说方言的模型”。看到 CosyVoice2 宣传”150ms 流式首包”,实测在这台机器上首包 11.97 秒——那个数字是 CUDA GPU 的,Apple Silicon 上完全不适用。看到性能宣传,先看它拿什么硬件测的。

4. 一个参数传错,她开始念稿子

CosyVoice 那阵还有个怪现象:8 个字的句子,生成 7.2 秒音频。听着像在念广播稿。

原因是角色的 ttsInstruct(一段 60 字的风格描述,”用很软很细的年轻女生声音说,甜一点、轻一点……”)被当成引擎指令传进去了。

CosyVoice 会把长指令当成要念的内容,演出来。

同一句话:

传什么 生成音频
只传方言指令 1.60 秒
加上角色风格描述 7.20 秒

音色本来就是从参考音频克隆的,那段文字描述一点用没有,纯粹在拖时长。

5. 配置改了不生效,因为背后有张兜底表在做主

我说方言没生效。它改的是 skins.json,反复确认改对了。

真相是:前端有张 BUILTIN_SKINS 硬编码兜底表,在 skins.json 加载失败时使用。而启动时那个 fetch 恰好总是失败

skins.json load failed — using builtin catalog

所以真正决定音色的一直是那张兜底表,被改的那个文件,从头到尾没被读到过。

教训:兜底逻辑要是和主逻辑不同步,它就不是兜底,是沉默的覆盖。现在那张表由脚本从 skins.json 生成,注释里写明必须同步。


三、同一个错,我签收了三次

上面五件都是判断错。这一节是方法错,也是这三天里最贵的一课。

三次的形状一模一样:

它写的那半截逻辑是对的,单元测试全绿,它跟我说”修好了”; 我信了,签收; 然后我打开 app 一看——还是那样。

前两次我以为是运气不好。第三次我才明白,问题不在它写得对不对,在于”全绿”这两个字从来就不覆盖我看见的那一块

第一次:嘴根本没动

它想让嘴型对上字。语音是自己合成的,知道每个字是什么,拼音的韵母决定口型(a 大张、i 扁、u 撅)。思路没问题,写完测:

好 开 心 呀 想 风
0.72 0.72 0.24 0.72 0.72 0.52

单元测试全绿。再直接调合成函数,实时采样:

0.96s → 0.07 1.20s → 0.71 1.56s → 0.72
1.08s → 0.48 1.32s → 0.26 1.92s → 0.52

数据这么漂亮,它交付了,我签收了。

然后我打开 app:嘴根本没动。

以为是数值太小,调大。还是没动。以为是分块时序有问题,重构。还是没动。装了三次,我看了三次,都是死的。

最后它挂了个 WebSocket 监听器,看前端到底收到了什么:

60 秒,收到状态 1 条,其中带口型值的 0 条。

这个功能在产品里从头到尾就没工作过

而每一轮所谓的”验证”,测的都是它刚写的那一段——单元测试是它写的,采样脚本也是它写的,两样东西加起来只能证明一件事:”我这半截是通的”。没有一次去看屏幕上那张脸。

而屏幕,只有我看得见。

第二次:算了一堆参数,没人消费

同一天,它给人物加了点头和视线漂移,参数算得好好的,装上去——我说没变化。

查下来,渲染器在当前模式下只读两个参数(呼吸、嘴型),头和视线的参数压根不看。算了一堆,下游根本没接。

parameters.ts 里字段都在,所以代码看起来完全正确。接口存在 ≠ 有人在用。

后来清理时把这些死代码删了,动画模块从 300 行降到 123 行。

第三次:数据库里 54 条,一条标记都没有

两个角色共享记忆,结果小月开口说”紫苏在这里听你”——她在模仿历史里紫苏的台词。逻辑改成”对话按角色分开,档案仍共享”,写了测试,全绿,交付。

我换到小月,说了两句:还在串。

一查数据库:

(无标记) | assistant | 54
(无标记) | user | 54

54 条对话,一条都没有角色标记。

后端一直接受 skin_id前端从来没发过。所以分离逻辑完全正确、单元测试全部通过,而真实数据里每一条都是”不知道谁说的”——无标记对所有角色可见,正好就是我要防的那种情况。

验证的是”分离逻辑对不对”,没人验证”真实数据里有没有标记”。

还有一次,连”恢复”都是假的

素材实验失败之后,git checkout 回退,命令跑通、日志正常,它跟我说”已恢复”。

我看了一眼:嘴还在动。

一量:差异 0.05 和 0.10,不是 0。回退到的那个提交,本身就已经包含了一次重建

同一个毛病的另一种长相——看到操作成功,就当成结果正确

这三次教会我的

一个能自证的功能,和一个我能看见的功能,是两回事。

验证要从最终呈现倒着往回查,不是从改动的那行正着往外推。 挂个监听器看真实数据流,比再写十个单元测试有用。

后来的规矩是:改完素材去量像素差异,改完状态推送挂监听器数消息,改完角色标记直接查数据库。命令的退出码只说明命令跑了,不说明事情做成了;测试全绿只说明它自己那半截通了,不说明我这头看得见。

道理写出来就这么一句话。可我是在第三次说出”还是那样”之后,才真的记住的。

也别全怪它。这三次的共同点是:它没有眼睛,而唯一有眼睛的那个人,三次都没在交付的时候睁开。


四、有一整类 bug,根子只有一个

翻 fix 列表,有一整类长得都一样:

fix: 环境噪音把每条回复都取消了
fix: 回复说到一半被 VAD 打断
fix: 整轮 STT/LLM/TTS 期间锁住麦克风
fix: 她在回答自己的声音

根源都是麦克风开得太早。她还在说,麦克风开了,听见自己的声音,触发新一轮。

最后一处特别隐蔽:正常流程会等播放结束才开麦,但异常路径直接 return,跳过了等待。任何一次报错,只要音频已经在队列里,她就会听见自己。

finally:

# 所有退出路径都要等,不只是顺利的那条
with suppress(Exception):
    await self._wait_playback_done()
self._turn_busy = False

教训finally 里该做的清理,别只写在顺风顺水的那条路上。


五、有些墙是结构,不是参数

眨眼调了三轮,一点用没有

眨眼一直很怪。它调过时长(280ms→150ms)、去掉闭眼停留、改混合曲线,每改一版我看一版,每一版都还是怪。

这一节是我们俩配合得最难受的一段:我只能说”怪”,说不出怪在哪;它拿到的输入永远只有”怪”这一个字,只好继续调参数。

最后才发现原因是结构性的:眨眼板是另外单独生成的一整张脸。所谓”眨眼”,实际是把整个头换成另一张略有不同的头——五官轮廓都跟着变一下。

同理,”点头”在这套架构里是整张立绘平移,看起来是整个人在滑动,不是点头。

它量了几张图里人物的实际像素范围:

人物尺寸
底图 981 × 1473
表情板 951 × 1438

差 3%。淡入时两个不同大小的她叠在一起——这就是我一直在说的”两张图叠在一起””闪一下”。我形容不出所以然,只能说”怪”;数字一摆出来,怪在哪儿就清楚了。

更糟的是各区域的差异:所谓的”嘴型板”,眼睛的差异(14.8)比嘴还大(11.4)。它们根本不是同一张脸配不同的嘴。交叉淡入时两套眼睛半透明叠加,眼睛就糊了。

修法是把表情板重新合成:只保留它该变的那一块,其余全用底图。修完眼部差异 0.02,嘴部 10.7。

教训:调了三轮参数都没效果的时候,该去看结构,不是继续调参数。这类问题的信号很明确——改动幅度和效果不成比例

破局的那一下也值得说:不是它想通了,也不是我看出来了,是它改去量像素。我出”哪儿不对”的直觉,它把直觉翻译成数字——这一节之后,凡是我说”怪”,它第一件事就是先量一遍再动手。

想让嘴动起来,试了四种,全废

角色立绘是 AI 生成的写实人脸。要让嘴动,就得有”张嘴”的那张图。四种做法它全试了,我一张张看过去:

方法 结果
几何拉伸下颌 脖子在动,嘴没动——另一个角色的嘴部坐标被套了过来,两人取景不同
沿唇缝切开、下唇下移、填暗部 能动了,我看一眼:”很诡异”
上唇取底图、开口取表情板 边界低了不张嘴,边界高了唇上一条白缝
Stable Diffusion 局部重绘 嘴唇掉色发白,牙齿畸形

四种失败,原因是同一个:

AI 生成的写实人脸,任何局部改动都很难和原图无缝。

它不是”一张脸配几个嘴”,是每次生成都重画了整张脸。所以切、拉、混、重绘——只要动了局部,就会露出接缝。

有个反例:另一个角色有真人手绘的口型板,她的嘴一直动得好好的。所以这不是技术不够,是素材本来就该是画出来的

这四次的判决书是同一句话,而且只能由我来写:我看了一眼,说不行。像素差异它量得出来,”这张脸看着像不像人”它量不出来——四次里有三次,代码和数字都是对的,废掉它们的理由全是”诡异””发白””一条白缝”。

画面这条路的边界到这里就很清楚了:没有手绘素材,别指望局部编辑能救。


六、记忆:我做的是存储,不是记忆

画面折腾不动了,转头做记忆——结果这是整个项目里性价比最高的一次改动。

一句话就戳穿了

第一版做完了:SQLite 存对话,重启后载入,角色之间共享一份。自测通过,交付。

我问了一句:「所以这个记忆只有 6 条,今天记不住昨天说过的话对吗?」

对。2000 条存在磁盘上,但每次只回放最近 6 条给模型。中间聊满 3 轮,昨天的就被挤出去了——数据还在,模型看不见。

做出来的是持久化,不是记忆。

而且那个 6 设得毫无道理。算笔账:

token
上下文上限 2048
人格 ~260
6 条对话 ~150
浪费 80%

实测 prompt 处理只要 0.02-0.05 秒,多塞几十条根本不影响延迟。那个 6 是凭”感觉会慢”设的,从来没人算过这笔账。

滑动窗口有天花板

窗口开到 24 条能跨天了,但仍然记不住”一周前说过我喜欢喝茶”。滑动窗口的本质就是会遗忘。

真正的解法是另存一份关于用户的稳定事实,每 8 轮后台重写一次,永远拼在人格后面。用真实模型跑出来是这样:

称呼:想风
身份:正在做 Lumi 开源项目的开发者
喜好:偏好喝茶
习惯:下周三要出差去上海

这是”她认识你”,不是”她记得刚才那句”。

最终三层:

作用 容量
档案 认识你 400 字,常驻
窗口 记得刚才 24 条,每次回放
数据库 查得到 2000 条,存磁盘

延迟没变,还是 2.5 秒。

教训:我要的是”记忆”,交上来的是”存储”,两者之间差一个”模型能不能看见”。功能名字对上了,不代表需求满足了。

提需求的人得把话说到底。我说”她要能记住我”,它听成”把对话存下来”——严格说它没做错,是我没说清”记住”是什么意思。


七、硬件的天花板:数字人这条路走不通

停下来之前,我让它把”这台机器上能不能做出真正的数字人”彻底查一遍。这种活它做得比我好得多——读支持矩阵、翻 issue、算 RTF,几十分钟就把三条路走完了。结论值得记下来,能替别人省很多时间。

三个方案,两个不通

方案 状态
EchoMimic V2(阿里) 官方环境是 CUDA 12.4,测试机 A100/4090D/V100,支持矩阵里没有 macOS。有第三方 MPS 指南,但依赖 Triton,而 Triton 在 macOS 上是实验性的、没有 Metal 后端
HeyGem(硅基智能) 要求 Windows 10 + RTX 4070 + Docker,容器是 CUDA 镜像,Mac 上没有路径
GPT-SoVITS ✅ 官方 README 有 M4 数据:RTF 0.526。2026 年有篇专门研究趟平了 MPS 的 7 个 fp16 坑并开源了补丁。粤语官方支持

一个被反复忽略的常识

有人跟我说:「48GB 统一内存完全够用,权重全部常驻内存,实现飞速推理。」

这句话我当时是信的——48 是个大数字,听着就够。把它拆开的是它,一张表:

内存容量决定能不能装下,算力和带宽决定跑多快。这是两回事。

M4 A100
内存 48 GB ✅ 更大 80 GB
带宽 ~120 GB/s 1935 GB/s
算力 ~4 TFLOPS 312 TFLOPS

48GB 这个优势,在扩散模型推理里几乎用不上。

决定性的那个数字

EchoMimic V2 的加速版,在 A100 上是 120 帧 / 50 秒

120 帧 ÷ 24fps = 5 秒视频,渲染要 50 秒 → RTF ≈ 10

在 A100 上就已经比实时慢 10 倍。 这说明它根本不是实时技术,是离线出片工具。M4 上保守估计 RTF 100+,她说一句 5 秒的话,要渲染 8 分钟起步

这跟”Mac 跑不跑得动”无关——就算买张 4090,它也不适合对话,只适合剪视频。

本机的实测佐证

不用推测,这台机器上就有数据:

类型 RTF
CosyVoice3 (0.5B) 音频 1.6 - 2.9
CosyVoice2 官方宣称 流式首包 150ms CUDA
同一模型本机实测 首包 11.97 秒

一个 0.5B 的纯音频模型,在这台机器上都跑不到实时。 视频扩散模型重几个数量级。

ComfyUI 也救不了——它是编排层,底层算子没有 Metal 实现,就是没有。

这一节是全篇唯一一处,我从头到尾没帮上任何忙。支持矩阵、Triton 有没有 Metal 后端、A100 上 120 帧要 50 秒——这些我自己查,一周也未必查得全,它几十分钟就摆完了,而且每个结论都带着出处。

分工到这儿反过来了:凡是有客观答案的问题,我最该做的就是闭嘴,让它去查。 我唯一的贡献是最后那个决定——知道了 RTF 100+,那就不做了。


八、最后一轮:五个实验,三成两否

撞完墙,我列了五件”可能让她更像人”的事,让它一件件试过去。价值不在成败,在于同样是”试试看”,成本和回报能差两个数量级

① 情绪进声音 —— 一个做了一半的功能

先读代码,它发现:

emotion = classify_emotion(reply)
self._emit({“type”: “companion.emotion”, …}) # ← 只发给了脸

情绪算出来了,只驱动表情,没进语音。而 TTS 的语气是切角色时设一次,之后再不变。

所以她说难过的话和开心的话用的是同一个语气——脸上是 sad,声音还是那个平稳的调子。

真人的情绪,首先在声音里。这不是”缺功能”,是功能做了一半没接上。接上花了二十分钟。

② 她每句都在反问

翻日志:

你想做什么呢? 有什么事情想要分享吗?

真人不会每句都把问题抛回来。这是模型在服务对话,不是在参与对话

改法:人格里加一句”不要每句都反问”,外加两条短回复示范。五个普通开场白实测:

改前 改后
我到家了 你今天过得怎么样呀? 嗯。
今天好累 发生什么事了吗? 那就早点休息。

反问率 5/5 → 1/5。 成本:一行字加两个例子。

③ 上下文:是我自己掐的

“num_ctx”: min(self.config.context_tokens, 2048)

模型支持 32768,代码里只给了 2048。当初为压延迟设的,可后来实测 prompt 处理只要 0.02-0.05 秒——这个限制从头到尾没换来任何东西。自己给自己戴的镣铐,戴久了就忘了是自己戴的。

④ 音色:本地其实能定制

一直以为只能从 9 个预设里挑,我一个个听过去,没一个像她。翻 API 才发现 Qwen3-TTS 有 VoiceDesign 变体——一句中文描述就能造音色,不要样本、不要微调:

成熟女性声音,略带沙哑,语速从容,尾音略上扬,有点慵懒。

代价是模型从 0.6B 换成 1.7B,但实测首音仍是 2.3 秒,感知没变慢。

这是唯一一次”以为不行、其实可以”。前面否掉 CosyVoice 和 VoxCPM2 之后,笔记里写过一句”本地音色到头了”——那个结论下早了。

顺嘴说一句:那句”成熟女性声音,略带沙哑,语速从容”,是我写的。这大概是整个项目里我唯一亲手写进代码的东西。

⑤ 换更大的模型 —— 直接否

之前测得 gpt-oss:20b 和 qwen2.5:7b 速度几乎一样(53.8 vs 50.8 tok/s),据此推断”参数量不是瓶颈”。实测 27B:

模型 速度 一轮
qwen2.5:7b 53.3 tok/s 1.22s
qwen3.5:27b 6.1 tok/s 26.72s

慢 9 倍,还返回空。 17.4 GB 超出了能高效驻留的范围,开始换页。

从 20B 到 27B 不是线性的,是掉下悬崖。 那个推断只在特定区间成立,往外一推就错。

最便宜的那一下,是一行字

我说:「她有点笨,分不清白天黑夜。」

查了下——提示词里从来没告诉过她现在几点。模型自己没有时钟。

加一行:

现在是星期二凌晨02:15,很晚了。

凌晨两点说「我还没睡」,之前她会答”早呀”,现在:

那陪你说说话就好啦。睡不着?试试深呼吸放松一下。

成本:一行字。效果:立刻像个懂事的人。

画面上试了四种方法全部失败,最后真正提升体验的,是这一行。而这一行之所以会被加上,只因为凌晨两点我坐在那儿,觉得她那句”早呀”听着别扭。

在对的地方花一分力,胜过在错的地方花十分力。

而”哪里是对的地方”,是撞出来的,不是想出来的。


九、两件差点让我翻车的工程小事

为了测一个模型,把正在用的搞坏了

为了试 VoxCPM2(能克隆音色、支持粤语),一句 uv pip install -U mlx-audio 直接打在主环境上。我在旁边看着,没拦。

这一下把 mlx 从 0.31.1 带到了 0.32。然后每一轮对话都炸:

RuntimeError: There is no Stream(gpu, 0) in current thread.

新版 mlx 要求调用线程自己持有 GPU stream,而我们的 Whisper 跑在 asyncio.to_thread 里。她彻底听不见任何话。

更麻烦的是只回退 mlx-audio 没用,报错变成 Stream(gpu, 2)——问题出在 mlx 本体,得按锁文件恢复整个环境。

而 VoxCPM2 最后还被否了:M4 上实测 RTF 1.02(现有方案 0.22),音色我一听就说不行。

为了一个最终被否决的测试,把正在用的东西搞坏了。 教训不是”别试新东西”,是别在主环境里试。现在 mlx 和 mlx-audio 都锁死了,注释写明了为什么。

这条也是对我说的:agent 装依赖不心疼,因为炸掉的不是它的环境。该拦的时候得拦。

改个名字,逼出三个没声明的依赖

把项目文件夹从”本地运行的AI女友”改名成 lumi,虚拟环境的绝对路径失效,只能重建。

重建完,测试跑不起来了——三个依赖没在 pyproject.toml 里声明websocketspillowopenai

旧环境里它们碰巧存在(被别的包顺带装上的),所以三天里从来没暴露过。任何人 clone 仓库跑 uv sync 都会踩到。

这也是”只有真实环境说了算”的又一个例子:机器上跑得通,不等于仓库里写全了。

改名这件事本身没什么技术含量,但它强制了一次干净安装,顺手验证了”新人能不能跑起来”

想开源的项目,建议主动做一次:删掉虚拟环境,从零装一遍。


十、现在的样子,和为什么停在这里

麦克风 → VAD → Whisper → qwen2.5:7b → Qwen3-TTS → 扬声器

说完到她开口 约 2.5 秒
识别 0.45s / 3 秒音频
思考 0.8s
首句合成 ~1.2s(RTF 0.24-0.35)
记忆 档案 + 24 条窗口 + 2000 条磁盘
角色 两个,开机随机
联网

代码量:Python 8700 行、TypeScript 5400 行、Rust 2100 行,测试 199 + 86 + 32。

画面停在静态立绘 + 音量驱动的嘴。 不炫,但它是真的在动,而且不会让你等 8 分钟。

为什么停

不是做不下去,是投入产出比变了

语音那条链路已经能用,再往下的每一步——分层立绘、GPT-SoVITS 微调——都是美术工作量或独立环境搭建,不再是写代码能推进的。而真正想要的”数字人视频”,在这台机器上是物理不可能,不是努力不够。

知道边界在哪,比在边界上反复撞更有价值。

如果要继续,只有三条

  1. 分层立绘

    ——把眼皮、嘴切成独立图层。这是这台机器上画面的唯一出路,做出来是”能动的立绘”,不是数字人视频

  2. GPT-SoVITS

    ——声音定制 + 粤语,唯一有 M4 实测数据支撑的方案

  3. 加硬件

    ——局域网放台带显卡的机器,或者租云 GPU 离线出片

前两条我都留了路,第三条不在这台机器的范围内。


十一、三天下来最想说的

1. 日志比直觉快十倍

那个”空回复”的问题,答案在日志里躺了很久。我们在猜、在做基准、在下 9GB 模型的时候,ASSISTANT: (empty) 就明明白白写在那儿。

后来我发现连日志本身都是被丢掉的——守护进程被窗口启动时,stdoutstderr 都设成了 null从 app 启动,所有诊断信息都进了黑洞,只有从终端启动才看得见。

修好日志,问题当天就定位了。

2. 测量要测对东西

tokens/秒 不等于有用输出。RTF 1.5 在 GPU 上和在 Apple Silicon 上是两个世界。宣传数字,先看测试条件。

3. 有些判断只有人能做——这是我在这个项目里的全部价值

eric 是不是男声、5 秒能不能忍、”很诡异”到底诡异在哪、凌晨两点该不该说”早呀”——这些它给不出答案。

它能测速度、能读元数据、能跑测试、能一小时读完三个项目的支持矩阵,做这些比我快得多。但它没有耳朵,也没有眼睛:听不见音色,看不见屏幕上那张脸动没动。

所以这三天真正的分工是:它负责所有能被验证的事,我负责所有只能被感觉的事。

问题在于,我一次次把”能被感觉的事”也当成能被验证的事,等着测试全绿来告诉我。测试当然全绿——它只覆盖它自己那半截。

这类判断,越早给出去越省钱。它猜完再返工,比我当场听一句贵十倍。

4. 做不出来就删掉

口型删了,眨眼删了,点头视线删了,方言删了,legacy 语音链路 750 行也删了。

留下的是真能用的:听得见、想得动、说得出、关得掉、不会自言自语。

一个删掉的功能不丢人,一个装了三次都不工作的功能才丢人。

5. 操作成功 ≠ 结果正确

这是这三天最贵的一课,前面讲了三遍,不重复了。只留一句给以后的自己:

“它那半截通了”和”我这头看得见”,是两回事。

以后凡是它说”修好了”,我只回一句:我怎么看出来?答不上来,就不算修好。

6. 知道边界,比撞边界有用

这三天最值钱的产出不是代码,是一张”什么能做、什么不能做”的地图:

  • 视频数字人在任何消费级硬件上都不是实时技术 ← 省掉一周

  • 整图贴片架构做不出眨眼和点头 ← 省掉反复调参

  • 48GB 内存解决不了算力问题 ← 省掉一次选型错误

7. 66 : 34

修 bug 是加功能的两倍。而且这还是个三天的新项目,没有历史包袱。

我原本以为这个比例说明活干得不好。后来想明白了:语音、音频、进程生命周期这类东西,本来就是靠一个个真实故障逼出来的。每个 fix 的提交信息里都写清楚了”坏掉的时候是什么样”——那些才是这个项目真正的资产。

而那 75 次”坏掉的时候是什么样”,几乎每一条的起点,都是我坐在电脑前说的一句很土的话:不对、很怪、听着别扭、还是那样。

代码它写得比我快。可这句”还是那样”,得我来说。


附:汇总七条

  1. 日志比直觉快十倍

    ——答案常常已经躺在那儿了,只是没人去看

  2. tokens/秒 不等于有用输出

    ——跑基准前先确认被测的东西真的产出了东西

  3. 能被验证的事交给 agent,只能被感觉的事必须自己来

    ——它没有耳朵,也没有眼睛

  4. 技术上可行 ≠ 产品上可用

    ——动手前先问”慢多少你能接受”

  5. 它说”修好了”,我只回一句:我怎么看出来?

    ——答不上来就不算修好

  6. 调三轮参数都没效果,就该去看结构

    ——改动幅度和效果不成比例,是结构问题的信号

  7. 知道边界比撞边界有用

    ——三天最值钱的产出不是代码,是一张”什么能做什么不能做”的地图

最后、以上由claude code 编写本人审阅读,有任何问题可以评论区找我,我是想风。一个关注AI和出海的游民,欢迎关注我 @xaiwind 一起探讨!
本文首发想风技术文档本文首发想风技术文档本文首发想风技术文档

本作品采用《CC 协议》,转载必须注明作者和本文链接
唯有坚持,滴水穿石----will
zhaocrazy
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
编程AI 出海 @ 数字游民
文章
84
粉丝
29
喜欢
68
收藏
152
排名:502
访问:1.7 万
私信
所有博文