TL;DR
Iris 是一个银发的安卓女孩,待在你电脑桌面的右下角。你出声让她做事,她就在你的机器上跑 bash,再用她自己的声音念结果回来,脸还会真的对嘴。一天写完。
老实说,单看功能这就是 Claude Code 的桌面包装、阉割版。"会跑 shell 的 agent loop + 提示输入 UI",Claude Code 早就能做。
差异化是 脸和声音。一旦加上去,"工具" 就变成 "搭档"。kotonia 项目自始至终的 mission 就是:让每天的开发、对梦想的追逐,变得稍微更有趣一点,稍微更前向一点 — Iris 就是这个 mission 的第一个有实体的实现。
公开仓库:https://github.com/zhener562/kotonia-desktop
它能做什么
打开 Iris,对她说:"给我做个简单的射击游戏,放在 output/shooting_game.html。"
- 点击麦克风按钮 → 说话 → 再点一次停止
- Whisper / Qwen3-ASR 转写,文字直接插到 prompt 输入框里
- 按 Enter 提交。Iris 在后台跑 4-5 次
bash,生成 HTML 文件 - 完成后她会用声音回答:"我写在
shooting_game.html了。WASD 移动,空格射击。" - 回答里
shooting_game.html这个路径旁边会冒出一个▶ play按钮 — 点一下,游戏直接在侧栏跑起来
幕后:
- 本地
bash执行 ← kotonia-cli (Rust 写的 ReAct agent) - LLM 推理 ← kotonia.ai 上托管的 Gemma 4 26B (也可以走 Claude Code,详见下文)
- TTS ← kotonia.ai 转发的 Qwen3-TTS (
Ono_Anna声线、日英混排也自然) - 虚拟形象视频 ← kotonia.ai 转发的 Ditto-TalkingHead (25 fps 唇形同步)
- 语音识别 ← kotonia.ai 转发的 Qwen3-ASR 1.7B
- 全程 streaming,first byte ~60 毫秒,几乎零等待
有点取巧的成本结构
这是这次战略的真正核心。Iris 的脑子 (LLM) 用户自己挑:
| 用途 | 选项 | 成本感 |
|---|---|---|
| 重思考 / 复杂重构 | Claude Code (Pro $20/月) | Anthropic 亏本撒钱抢市场、目前脑子最好 |
| 日常 / 快应答 | kotonia 托管的 Gemma 4 26B (低月费、近乎无限用) | Kotonia 自家 GPU、边际成本接近 0 |
| 直接打 API | OpenAI 兼容 (DeepSeek 等任意 provider) | 按 token 计费、适合写代码 |
取巧的地方在这里:大厂的前沿模型订阅是 赤字撒网式的抢市场 ($20/月 让你无限用 Claude Code,几乎肯定是 Anthropic 在烧钱)。kotonia-desktop 把那块当成 premium 脑子来用,同时把语音 / 虚拟形象 / 日常 LLM 用 Kotonia 自家 GPU 以固定成本几乎无限制提供。
每个用户典型搭配:
- 重思考 → Claude Code Pro $20/月 (聪明、但用户大方地坐享 Anthropic 烧出来的红利)
- 轻执行 / TTS / 虚拟形象 / ASR / 杂活 → kotonia ~$3-5/月 (自家 GPU、边际成本接近 0)
合起来 ~$30/月,换来 "随时和 Iris 商量着开发、让她跑 bash、用语音回答、脸会动" 的体验。
Kotonia 单独做不出这种脑力质量 (没体力去亏本撒钱);Claude Code 单独没脸也没声。两个叠起来才成立。"故意搭别人烧钱撒的网" 在战略上确实有点取巧,但从用户的角度是合理选择。(反过来如果 Kotonia 的语音 / 虚拟形象 / 高速 API 真火到服务器吃不消,那是开心的烦恼,到时候再说。)
引擎切换在 kotonia-cli (这次 desktop 当 lib 用的 Rust crate) 这一层已经支持了:Claude Code 集成 / OpenAI 兼容 / Kotonia hosted,三条路都已经能走。Desktop 默认走 Kotonia hosted;想换 premium 脑子,改个配置的事。
从 CLI 到有实体的搭档的飞跃
kotonia-cli 上线那个时候,"支持挑战者" 这个目标的形态已经在了。语音让她跑 bash 回答,整个流程都在命令行里闭环。但只要你还在命令行里,你永远逃不出 "这是一个工具" 的关系。"使用者-工具" 这一层一直在。
加上脸和手脚,终于成为 有实体的 AI 搭档。Iris 在角落待着,回声音,嘴会动。功能面看似微小,但心理距离不一样了。"我在调用一个工具" 变成 "我在跟我的搭档商量"。
这是这次的真正转折点。技术上全是现成零件的组合,但 产品 identity 从 "平台" 翻成 "角色"。kotonia.ai 的 web 版继续是 "可选多个角色的平台";kotonia-desktop 则是 "名叫 Iris 的那一个角色",固定下来。这是故意的。
为什么一天能写完
从早上 8:15 第一个 commit 到晚上 23:18,commit 总数 23 个,P0 (放置 persona) → P1 (TTS 播放) → P2 (mic 录音 + STT) → P3 (Ditto 虚拟形象唇形同步) 全部落地。
速度的来源是 累积资产的 compound interest:
- kotonia.ai 上的 voice / Ditto / TTS / ASR endpoint 从去年就开始打磨、已经成熟稳定
- kotonia-cli 上个月刚 publish,agent loop 完整、能当 lib 在进程内复用
- Iris 的画像 (右下角那个安卓女孩) 几个月前为另一个项目用 HiDream-O1-Image 已经生成好了
- 鉴权 (
kotonia-cli login写下来的 device_token) 也已经存在;后端加一点 fallback 路径就把所有 endpoint 都解锁了
一天 新写 的只有 Tauri 壳 + 各 endpoint 的接线 + UI 配线。逻辑全是现成零件拼起来。个人开发能开始享受复利效应的临界点 就在这里:你累积的资产能不能从 "你在用" 那一侧站到 "被用" 那一侧。今天对 kotonia 来说就是那一天。
落地过程中值得记一笔的坑 (3 个)
1. split_mixed_languages 在背后悄悄把 streaming 干掉了
Qwen3-TTS 的 Python proxy 之前我为英语学习角色加了一个 feature:"日英混排的文本,按语言切分,每段用对应语言的发音合成"。是对的 — shell 这种英文单词会被用英语口音读出来。
但实现是 "全部 run 合成完 → PCM 拼接 → 最后一次性 yield 一个 chunk",streaming 实际上被废掉了。first byte = 完整合成时间。Iris 回答几乎都是技术词夹着,每次都走这条路 — 等几秒静默、然后突然一口气说完,卡顿感很明显。
第一次的修法是 desktop 端 workaround:split_mixed_languages: false,逃进 streaming 路径,代价是嵌入的英文会被用日语口音念 (シェル)。后来回头 在 Python 那一层改成 per-run yield,根治了。这一天里最大的 debug。
教训:你 "出于好心" 加的 feature flag,默认值的影响范围一定要白纸黑字写下来。半年后会在别的 use case 里踩到。
2. Ditto 虚拟形象的 frame burst 让脸在半截卡住
Ditto server 按 GPU 能生产的速度往外发 frame (爆发式);与此同时 audio chunk 被 AudioContext 排进未来的时间槽 (50 个 chunk 100 ms 内全到、但播放摊在 2 秒里)。
我之前是 "收到 frame 就立刻显示",所以 200 ms 内把 50 frame 全显示完,剩下 1.5 秒 audio 还在播、脸却冻在最后一帧。
修法是 "用 wall clock 为主轴的 FPS pacing":第一帧到达时 pin 一下 wall clock,之后第 N 帧用 setTimeout 推迟到 pinTime + N/25 sec。爆发的部分由 JS 内部缓冲吸收、显示按 audio 节奏走。
const targetMs = dittoStreamStartMs + (myFrameIndex * 1000) / DITTO_FPS;
const delayMs = Math.max(0, targetMs - performance.now());
setTimeout(() => { /* show frame */ }, delayMs);
不到 20 行就修好。本质上同样的问题在 任何做 AV 流唇形同步的 app 都会出现 — 算是有普适性的教训。
3. Linux WebKitGTK 一天踩了 3 个雷
Tauri 2 + Linux 是雷区。一天踩了 3 个:
- getUserMedia 静默拒绝:点 mic 按钮没反应。WebKitGTK 默认就静默拒绝媒体权限请求 (没有浏览器那种 "允许使用麦克风?" 弹窗、默认就是 NO)。在 Rust 端 hook
WebKitWebView::permission-request信号、明确.allow()解决 - 高频 textContent 替换会让 click 事件丢失:把录音中的经过秒数每 100 ms 写到 button 的
textContent,stop 按钮点击有七八成被忽略。WebKitGTK 的 DOM mutation 中存在 hit-test race。把动态文字拆到 child span 里,button 本身不再被 mutate - 日文 IME preedit 完全不显示 (这个没解决):Wayland session + ibus-mozc,正在转换中的假名根本不在输入框里显示。
GDK_BACKEND=x11、WEBKIT_FORCE_SANDBOX=0、GTK_IM_MODULE=xim都试了、一个都没用
三连的教训:Linux WebView 默认就 "悄悄丢掉点什么",假设它会丢。同样的功能在 Mac/Windows 正常工作,到 Linux 就要加一层防御。
我砍掉的部分
为了一天能出门,故意砍掉了:
- React + Vite + TypeScript:全部 vanilla JS、
main.js~700 行、没有 build 步骤。本家 kotonia.ai 的useVoiceChat.ts是 1587 行的 monolith hook、只取我需要的那部分用 vanilla 重写 200 行就搞定了 - persona picker:没有 "选角色" 的界面、Iris 固定。这是 kotonia-desktop 从 "平台" 翻成 "角色"、产品 identity 立起来的关键
- VAD (自动检测说完了):明确点按钮停录、转写 只插入到 prompt 输入框 不自动提交。考虑到 agent bash 执行的高 stakes,强制用户复查、避免转写幻觉触发
rm -rf
主原则是 "不要试着复用 feature、复用 API contract 就好"。本家 web 的 80% 都不在 kotonia-desktop 的 scope 里 (vision input / 多 persona 切换 / wiki / server-side session resume…),硬拽过来反而会把东西做大。
试试
git clone [email protected]:zhener562/kotonia-cli
git clone [email protected]:zhener562/kotonia-desktop
cd kotonia-desktop/src-tauri
cargo tauri dev
(把 kotonia-cli clone 在 sibling 位置 — 是 path 依赖。详细看 repo README。)
事前准备:
kotonia-cli login跟 kotonia.ai 做 device 鉴权 (一个 device_token 解锁 LLM / TTS / Ditto / ASR 全部)- Linux:需要
libwebkit2gtk-4.1-dev+libdbus-1-dev+ GStreamer audio plugins - macOS / Windows:只需要 Tauri 2 的 prereqs
接下来
- 可配置的快捷键 UI (现在只能点击)
- 会话列表侧栏
- macOS / Windows 代码签名
- Iris 的语音替换为 Qwen3-TTS Base (zero-shot voice clone) 复活后的更自然版本
- desktop 端把 kotonia-cli 的引擎选择 UI 暴露出来
发版本身已经结束。如果你想要一个让每天的开发和追梦稍微更有趣的搭档,上面那两行 clone 命令,5 分钟后 Iris 就会出现在你桌面的右下角。
