22 KiB
22 KiB
讨论记录 (taolun.md)
2026-04-24 - 项目启动
用户需求
基于 Mimo-TTS API 开发 Rust CLI 工具,要求:
- Rust 编写,单一二进制可执行文件
- 后期通过 skill 给其他 claw 工具使用
- CLI 参数调用方式
- OOP + 设计模式架构
- 全程中文沟通
- 详细中文注释
- 每次操作前先更新文档
API 信息
- Endpoint:
POST https://api.xiaomimimo.com/v1/chat/completions - 认证:双 Header (
api-key+Authorization: Bearer) - 音频格式:WAV(Base64 编码)
2026-04-24 - 实施记录
第一轮完成
- ✅ 创建三个文档(taolun.md、changelog.md、agents.md)
- ✅ 配置 Rust 国内源(稀疏索引协议)
- ✅ 实现项目代码(config.rs、api.rs、cli.rs、main.rs)
- ✅ Debug + Release 构建成功
- ✅ 基本功能测试通过
第二轮修改
用户新增需求:
- project.config.toml: 项目根目录,含 version,与 git version 一致
- 统一配置路径:所有平台使用
~/.config/tts/config.toml - API Key: sk-csa6s3bvpwqw21zfs3urbmqgrw720eoa1qdz6o42abk5xoj8
- onboard 命令:CLI 引导式配置初始化
执行内容:
- ✅ 创建
project.config.toml(version = "0.1.0") - ✅ 修改
Cargo.toml(移除 dirs,添加 home 依赖) - ✅ 修改
config.rs(统一路径为~/.config/tts/config.toml) - ✅ 修改
cli.rs(添加 Onboard 子命令) - ✅ 修改
main.rs(处理 Onboard,恢复 main() 函数) - ✅ 配置 API Key 成功
- ✅ 语音合成测试成功(test.wav、final_test.wav)
- ✅ Release 构建成功(无警告)
踩坑记录
-
Cargo 国内源超时
- 解决:改用稀疏索引协议(sparse+https://)
-
clap derive 模式缺少 Parser trait 导入
- 解决:添加
use clap::Parser;
- 解决:添加
-
main() 函数丢失
- 原因:编辑时不小心删除
- 解决:恢复 main() 函数和 ExitCode 枚举
-
配置文件路径统一
- 需求:所有平台使用
~/.config/tts/config.toml - 解决:使用 home 库获取家目录,手动拼接路径
- 需求:所有平台使用
项目最终状态
- 位置: /Users/titor/tts
- 二进制: target/release/mimo-tts (4.5MB)
- 配置: ~/.config/tts/config.toml(已配置 API Key)
- 项目配置: project.config.toml (version = "0.1.0")
- 功能:
- ✅ TTS 语音合成
- ✅ 配置管理(config set/show)
- ✅ 引导式配置(onboard)
- ✅ 列出音色(voices)
- 架构: Builder 模式 + Singleton 模式 + OOP
退出码规范
- 0: 成功
- 1: 参数错误
- 2: 配置错误
- 3: API 调用失败
- 4: 文件操作失败
2026-04-24 - 第三轮修改
用户新需求
不要保存语音到本地,使用流式数据输出
- 语音数据直接输出到 stdout(二进制流)
- 便于其他程序通过管道读取
- 更适合作为 claw skill 集成
修改计划
- 修改
synthesize()函数,返回音频数据而不是保存文件 - 修改
main.rs,支持输出到 stdout(二进制模式) - 保留可选的
--output参数用于保存到文件(向后兼容) - 当输出到 stdout 时,不输出其他文本信息(避免污染二进制流)
- 流式调用可能需要使用 pcm16 格式(根据 API 文档)
2026-04-24 - 第四轮修改
用户新需求
添加音频播放功能
- 使用 rodio 库直接播放音频
- 添加 --play 参数触发播放模式
- 默认单次播放,不循环
- 保留 --output 和 stdout 输出功能
技术选型
- HTTP 客户端:reqwest(已使用)
- 异步运行时:tokio(已使用)
- 音频播放:rodio 0.19
- WAV 处理:无需额外库(rodio 直接支持)
2026-04-24 - 第四轮实施完成
已完成
- ✅ 修改 Cargo.toml 添加 rodio 0.19 依赖
- ✅ 修改 cli.rs 添加 --play 参数
- ✅ 修改 main.rs:
- synthesize() 返回 Vec(音频数据)
- 添加 play_audio() 函数(使用 rodio 播放)
- 修改 run() 支持三种输出方式(播放/保存/stdout)
- ✅ 修复编译错误(synthesize() 重复代码、未使用导入)
- ✅ 修复 stdout 输出污染问题(移除 synthesize() 中的 println)
- ✅ Release 构建成功(无警告)
- ✅ 功能测试通过:
- --play 播放音频 ✅
- --output 保存文件 ✅
- stdout 二进制流输出 ✅
最终项目状态
- 二进制文件:target/release/mimo-tts (约 6MB,包含 rodio)
- 支持三种输出方式:播放、保存、流式输出
- 所有功能测试通过
2026-04-24 - 第五轮修改
用户确认需求
扩展音色支持
- ✅ 更新音色列表为 Mimo-TTS 完整列表(8个音色)
- ✅ 默认音色从 default_zh 改为 mimo_default
- ✅ 添加音色验证,无效时使用默认音色
- ✅ 废弃 default_zh 和 default_en
完整音色列表
| Voice ID | 语言 | 性别 | 说明 |
|---|---|---|---|
| mimo_default | 多语言 | - | MiMo默认(中国集群=冰糖) |
| 冰糖 | 中文 | 女性 | 甜美清脆的女声 |
| 茉莉 | 中文 | 女性 | 温柔知性的女声 |
| 苏打 | 中文 | 男性 | 阳光活力的男声 |
| 白桦 | 中文 | 男性 | 沉稳大气的男声 |
| Mia | 英文 | 女性 | Sweet and gentle female |
| Chloe | 英文 | 女性 | Warm and articulate female |
| Milo | 英文 | 男性 | Energetic young male |
| Dean | 英文 | 男性 | Deep and steady male |
2026-04-24 - 第五轮修改(修复)
问题修复
- 默认音色未生效:config.rs 中
default_voice()函数返回值仍是default_zh - 修复:将
config.rs:39默认值改为mimo_default - 更新配置文件
~/.config/tts/config.toml中的default_voice - 验证:show-config 现在正确显示
mimo_default
2026-04-24 - 第六轮修改
用户需求
UI 主题化所有 CLI 输出
- 使用 ratatui + crossterm 美化 CLI 输出
- onboard 改为完整表单页面(同时显示所有配置项)
- API Key 输入支持隐藏(显示 * 代替)
- 所有命令输出都使用主题美化
- 不需要简洁模式
实施方案
- 新增 src/ui.rs 模块(主题、组件)
- 修改 Cargo.toml 添加 ratatui + crossterm
- 美化 list_voices()、show_config() 输出
- 重写 onboard() 为完整交互式表单
- 美化所有命令输出(播放、保存等)
第六轮实施完成
- ✅ 创建 src/ui.rs 模块(使用 crossterm 美化输出)
- ✅ 修改 main.rs 引入 ui 模块
- ✅ 美化 list_voices() 输出(彩色表格)
- ✅ 美化 show_config() 输出(彩色标签)
- ✅ 重写 onboard() 为交互式表单(支持密码隐藏输入)
- ✅ 美化播放和保存完成消息
- ✅ Release 构建成功(仅有未使用代码警告)
- ✅ 功能测试通过(voices、show-config)
技术细节
- 使用 crossterm 而非完整的 ratatui Terminal(简化实现)
- 密码输入使用 enable_raw_mode 实现字符隐藏
- 输出使用颜色主题(PRIMARY、SUCCESS、ERROR 等)
- show_onboard_form 返回 Result 类型,由调用者处理错误
第七轮修改
用户需求
配置分层设计
project.config.toml- 项目默认配置(全量配置)- version
- base_url
- default_format
~/.config/tts/config.toml- 用户配置(仅覆盖项)- api_key
- default_voice
- 临时文件只允许存放在当前目录下
实施方案
- 修改
project.config.toml添加 base_url 和 default_format - 重写
config.rs实现分层配置加载 - 修改
cli.rs移除 base_url 参数 - 修改
ui.rs简化显示和表单(只处理 api_key 和 default_voice) - 修改
main.rs使用新的配置结构 - 测试时临时文件输出到当前目录
第七轮实施完成
- ✅ 创建分层配置结构(ProjectConfig + UserConfig + Config)
- ✅
project.config.toml包含默认配置(base_url、default_format) - ✅ 用户配置只保存 api_key 和 default_voice
- ✅ 修改 cli.rs 移除 base_url 参数
- ✅ 修改 ui.rs 简化显示和表单
- ✅ Release 构建成功(仅有未使用字段警告)
- ✅ 功能测试通过(show-config、voices、语音合成)
- ✅ 临时文件生成在当前目录
技术细节
- ConfigManager 先加载项目配置,再加载用户配置,最后合并
- save() 只保存 UserConfig(api_key、default_voice)
- project.config.toml 从当前项目目录读取
- 用户配置路径:
~/.config/tts/config.toml
2026-04-24 - 第八轮修改
用户反馈
使用 --voice 提示失败
- 问题:使用
--voice '茉莉'时报错:Unknown voice - 原因:之前使用了错误的 API 文档,实际 API 只支持 3 个预置音色
- 最新发现:mimo-v2.5-tts 模型支持更多音色(9 个)
- 模型名应为
mimo-v2.5-tts(不是mimo-v2-tts)
音色列表(mimo-v2.5-tts)
| Voice ID | 语言 | 性别 | 说明 |
|---|---|---|---|
| mimo_default | 多语言 | - | MiMo默认(中国集群=冰糖) |
| 冰糖 | 中文 | 女性 | 甜美清脆的女声 |
| 茉莉 | 中文 | 女性 | 温柔知性的女声 |
| 苏打 | 中文 | 男性 | 阳光活力的男声 |
| 白桦 | 中文 | 男性 | 沉稳大气的男声 |
| Mia | 英文 | 女性 | Sweet and gentle female |
| Chloe | 英文 | 女性 | Warm and articulate female |
| Milo | 英文 | 男性 | Energetic young male |
| Dean | 英文 | 男性 | Deep and steady male |
新增功能
--style参数:风格描述(如:开心、东北话、孙悟空、唱歌等)- 风格通过
user消息传递(不是<style>标签) - 模型名更新为
mimo-v2.5-tts
第八轮实施完成
- ✅ 更新模型名为
mimo-v2.5-tts - ✅ 恢复 9 个音色列表
- ✅ 添加
--style参数支持 - ✅ 修改风格传递方式(用 user 消息而非
<style>标签) - ✅ 修复编译错误(删除多余代码块)
- ✅ 测试通过(默认音色、茉莉音色均正常)
- ✅ 临时文件生成在当前目录
2026-04-24 - 第九轮修改
用户需求
实现流式输出功能
--stream参数已在 cli.rs 定义,需要实现- 流式输出时格式自动改为 pcm16
- 使用流式 API 调用(Server-Sent Events)
- 输出到 stdout(二进制流)
实施方案
- 修改 api.rs 添加流式请求方法
- 使用 reqwest 的流式响应处理
- 处理 SSE(Server-Sent Events)格式
- 拼接音频数据后输出到 stdout
- 如果指定了 --output,则保存到文件
第九轮实施完成
- ✅ 修改 Cargo.toml 添加依赖(futures、tokio-util、reqwest stream feature)
- ✅ 修改 main.rs:
- synthesize() 函数添加 stream 参数
- 流式输出时自动使用 pcm16 格式
- 传递 stream 参数给 API 调用
- 支持 --stream 和 --play 同时使用(PCM16 转 WAV)
- ✅ 修改 api.rs:
- 修复 read_stream_response 函数(使用 futures::StreamExt)
- 正确处理 SSE 格式流式响应
- ✅ 新增 pcm16_to_wav() 函数(封装 WAV 头)
- ✅ Release 构建成功(仅有未使用代码警告)
- ✅ 流式输出测试通过(PCM16 数据正常)
- ✅ 流式播放测试通过(--stream --play 正常工作)
技术细节
- 流式 API 返回原始 PCM16 数据(无 WAV 头)
- 使用 futures::StreamExt 处理 reqwest 的 bytes_stream()
- SSE 格式解析:
data: {...}和data: [DONE] - 每个 chunk 包含 Base64 编码的音频数据
- PCM16 转 WAV:添加 44 字节 WAV 头(24000Hz, 16bit, mono)
- --stream 和 --play 同时使用时自动封装 WAV 格式后播放
2026-04-24 - 第十轮修改
用户需求
实现自动语气转换器(独立模块)
- 创建
src/tone.rs独立模块(高内聚低耦合) - 默认启用,无需额外参数
- 根据标点符号自动添加语气标签
- 支持整体风格标签和细粒度控制标签
语气映射规则
整体风格标签(文本开头)
- 包含
!→[激动] - 包含
?→[疑惑] - 包含
。→[平静](默认) - 多种标点组合 →
[激动 疑惑]等
细粒度控制标签(文本中间)
!→!(激动)?→?(疑惑)。→。(停顿)……→……(拖长音)
第十轮实施完成
- ✅ 创建 src/tone.rs 模块
- ✅ 实现 apply_tone() 函数(整体语气)
- ✅ 实现 insert_mid_tone() 函数(细粒度控制)
- ✅ 实现 analyze_tone() 函数(分析语气)
- ✅ 实现 has_tone_tag() 函数(检测已有标签)
- ✅ 修改 main.rs 引入 tone 模块
- ✅ 在 synthesize() 中调用语气转换
- ✅ Release 构建成功
- ✅ 单元测试通过(4 个测试)
- ✅ 功能测试通过(激动语气、疑惑语气)
2026-04-25 - 第十一轮修改
用户需求
实现守护进程(Daemon)模式,类似 Docker 架构
- 作为后台服务运行,使用 TCP Socket 监听
- 允许其他应用(包括 Web)通过客户端访问
- 实现类似智能音响的功能:客户端发送文本,守护进程执行 TTS 并播放
- 所有平台统一使用
~/.config/tts/目录 - 支持 style 参数作为宏观场景风格(与文本内
[style]标签并存)
技术选型
- Socket 方案:TCP Socket(跨平台通用,所有平台统一)
- 协议:JSON over TCP(简单、易调试)
- 日志路径:
~/.config/tts/ttsd.log(所有平台统一) - PID 文件路径:
~/.config/tts/ttsd.pid - 默认端口:9876
新增文件
src/daemon.rs- 守护进程模块- TCP Socket 服务器(tokio::net::TcpListener)
- 处理客户端请求(JSON 协议)
- PID 文件管理(启动/停止)
- 日志记录(文件 + stdout)
- 调用 TTS API + 播放音频
src/client.rs- 客户端模块- TCP 客户端(tokio::net::TcpStream)
- 发送 TTS 请求到守护进程
- 接收并处理响应
修改文件
Cargo.toml- 添加
dirs = "5.0"(跨平台配置目录) - 添加
chrono = "0.4"(日志时间戳)
- 添加
src/cli.rs- 添加
Daemon子命令(start/stop/status) - 添加
Send子命令(发送文本到守护进程) - 支持
--port参数(默认 9876)
- 添加
src/main.rs- 声明新模块
daemon和client - 添加命令分发逻辑
- 声明新模块
协议设计
// 客户端 → 服务端
{
"text": "要合成的文本",
"voice": "mimo_default",
"format": "wav",
"style": "开心"
}
// 服务端 → 客户端
{"status": "ok", "message": "播放完成"}
{"status": "error", "message": "错误信息"}
style 参数说明
- 文本内标签:
[开心]你好!(细粒度,句子级别) - style 参数:宏观场景风格(如"新闻播报"、"开心")
- 服务端处理:如果有 style 参数,自动添加
<style>...</style>标签到文本开头(符合官方文档) - 两者可并存:style 参数 + 文本内标签同时生效
第十一轮实施完成(继续实施 - 2026-04-25)
已完成:
- ✅ 修改 Cargo.toml 添加依赖(dirs、chrono)
- ✅ 创建 src/daemon.rs(TCP 服务器、请求处理、PID 管理、日志)
- ✅ 创建 src/client.rs(TCP 客户端、请求发送、响应处理)
- ✅ 修改 src/cli.rs(Daemon、Send、ttsd 子命令)
- ✅ 修改 src/main.rs(模块声明、命令分发、spawn_daemon_process)
- ✅ 编译成功(仅有未使用代码警告)
最新测试结果(2026-04-25):
- ✅
mimo-tts ttsd --port 9876- 守护进程模式启动成功 - ✅
nohup ./mimo-tts ttsd --port 9876 &- 后台运行成功 - ✅
mimo-tts send --port 9876 "消息"- 发送请求成功,返回"播放完成" - ✅ 日志正常:
~/.config/tts/ttsd.log
用户需求:
- 启动:
mimo-tts daemon start -d --port 9876或mimo-tts daemon start -d - 停止:
mimo-tts daemon stop(无需端口) - 状态:
mimo-tts daemon status(无需端口)
当前方案:
- 使用
nohup启动后台进程(Unix) ttsd子命令作为内部守护进程模式stop和status无需端口参数
尚未完成:
daemon start -d命令(需要实现 spawn_daemon_process)daemon stop命令daemon status命令
2026-04-25 - 第十二轮:日志格式升级
用户需求
升级日志格式,添加级别和 PID 信息:
[时间戳] [级别] [PID] 消息- 级别自动检测(INFO/WARN/ERROR)
- 与旧日志区分(追加新格式)
实施完成
- ✅ 修改 daemon.rs:
- 添加 LogLevel 枚举
- 自动检测消息中的关键词判断级别
- 新格式:
[时间戳] [级别] [PID] 消息 - stdout 输出简化:
[Daemon PID] 消息
- ✅ 添加
daemon logs命令- 支持
--lines N参数 - 默认显示 20 行
- 支持
- ✅ 测试通过:
- 守护进程启动/停止/状态 ✅
- send 发送播放 ✅
- logs 查看日志 ✅
- 新格式正确显示 ✅
新日志格式
[2026-04-25 05:48:05] [INFO] [15278] 正在播放音频...
[2026-04-25 05:48:10] [INFO] [15278] 响应: {"status":"ok","message":"播放完成"}
级别自动检测
INFO- 默认(正常运行信息)WARN- 包含"警告"、"注意"ERROR- 包含"错误"、"失败"、"无法"
2026-04-25 - 第十三轮:添加 HTTP 接口
用户需求
让 Postman 可以测试守护进程:
- 添加 HTTP 接口(端口 9877)
- 支持 POST /synthesize 接口
- 支持 GET /health 健康检查
实施完成
- ✅ 添加 tiny_http 依赖
- ✅ 在 daemon.rs 添加 HTTP 服务器函数
- ✅ 自动启动 HTTP 服务器(端口 = TCP端口 + 1)
- ✅ 测试通过:
curl http://127.0.0.1:9877/synthesize✅curl http://127.0.0.1:9877/health✅
HTTP 接口设计
| 地址 | 方法 | 说明 |
|---|---|---|
http://127.0.0.1:9877/synthesize |
POST | 语音合成 |
http://127.0.0.1:9877/health |
GET | 健康检查 |
端口说明
- TCP: 9876 - 程序客户端用
- HTTP: 9877 - Postman/调试用(TCP端口 + 1)
Postman 测试示例
# 合成接口
curl -X POST http://127.0.0.1:9877/synthesize \
-H "Content-Type: application/json" \
-d '{"text":"你好世界"}'
# 健康检查
curl http://127.0.0.1:9877/health
踩坑记录
-
clap 参数传递问题
- 问题:
--port参数无法传递给daemon start子命令 - 原因:clap derive 模式中,参数定义在父命令,需要在子命令之前使用
- 解决:修改
DaemonCommand为独立结构体,使用正确的参数顺序 - 正确用法:
mimo-tts daemon --port 9876 start - 错误用法:
mimo-tts daemon start --port 9876
- 问题:
-
chrono 依赖缺失
- 问题:daemon.rs 使用
chrono::Local::now()但 Cargo.toml 未添加依赖 - 解决:添加
chrono = "0.4"到 Cargo.toml
- 问题:daemon.rs 使用
命令使用示例
# 启动守护进程(后台运行)
mimo-tts daemon --port 9876 start &
# 查看状态
mimo-tts daemon --port 9876 status
# 发送文本(使用 style 参数)
mimo-tts send --port 9876 --style "开心" "你好,世界!"
# 发送文本(不使用 style)
mimo-tts send --port 9876 "你好,世界!"
# 停止守护进程
mimo-tts daemon --port 9876 stop
技术细节
- 使用
tokio::net::TcpListener实现 TCP 服务器 - 使用
tokio::spawn处理多个并发连接 - 使用
serde_json序列化/反序列化 JSON 协议 - 使用
dirs::home_dir()获取家目录,拼接配置路径 - PID 文件用于检测守护进程是否运行
- 日志同时输出到文件和控制台
- 播放功能复用现有的
play_audio()函数 - style 参数按官方文档转换为
<style>...</style>标签
2026-05-09 - 代码质量提升
用户指令
开始修复之前评价中指出的问题。
修复清单
- ✅ HTTP /synthesize 接口缺少实际 TTS 调用 — 通过
tokio::runtime::Handle::current()+block_on()桥接 - ✅ 语气替换长匹配优先问题 — 调整
insert_mid_tone()中...优先于. - ✅ show_voices() 显示完整 9 音色 — 更新为完整列表含语言/性别/说明
- ✅ 9 个编译器警告清理 —
#[allow(dead_code)]+ 移除未使用变量/导入 - ✅ write_log 线程安全问题 — 添加
Mutex<()>静态锁 - ✅ changelog 版本结构整理 — 合并重复条目为 0.1.0→0.2.0→0.3.0→0.3.1
最终状态
- 版本:0.3.1
- 构建:0 warnings, 0 errors
- 测试:9/9 passed
2026-05-09 - 第十四轮修改:流式播放修复
背景
守护进程 send --stream 和 CLI --stream --play 均无声音输出,但非流式播放正常。
诊断过程
-
第一阶段:假设 RunLoop 问题
- 认为
rodio::OutputStream(!Send)在std::thread::spawn中因为 CoreAudio RunLoop 不可用导致无声 - 将 daemon 主循环改为
LocalSet+spawn_local,消除std::thread::spawn - 结果:仍无声 → 假设错误
- 认为
-
第二阶段:定位 SSE 解析问题
--stream(不带--play)测试:能输出 PCM16 数据(走 JSON 响应,不走 SSE)- 用 curl 直接查看 API 原始 SSE 响应 → 发现音频数据在
choices[0].delta.audio.data - 原
SseEvent只解析顶层audio字段 → 永远 None → 通道为空 → 静音 - 修复1:创建
SseChunk/SseChoice/SseDelta/SseAudio嵌套结构体 - 结果:仍无声
-
第三阶段:定位行缓冲问题
- 添加调试日志发现:SSE
data:行因音频 base64 过长,被 HTTP chunk 分割 - 行被切碎 → serde_json 解析失败 → 永远收不到数据
- 修复2:添加
line_buf跨 chunk 累积完整行再解析 - 结果:69KB PCM16 数据成功进入 channel,sink 正常播放 ✅
- 添加调试日志发现:SSE
技术细节
reqwest::Response::bytes_stream()的 chunk 边界不一定对齐 SSE\n- 长
data:行会跨 chunk 分割 - SSE 行缓冲器:按
\n切割,不完整行等下一个 chunk extract_audio_from_sse()爬choices[0].delta.audio.data
最终状态
- CLI
--stream --play正常出声 ✅ - Daemon
send --stream正常出声 ✅ - Daemon 非流式
send仍正常 ✅ - 版本:0.3.2(流式播放修复)