C++实现Whisper+Kimi端到端AI智能语音助手
来源:程序员老廖
第1章 项目概览与运行配置
1.1 项目背景与定位
为什么需要这个项目?
voice_ai_chat 是一个端到端语音对话系统,它将两大核心能力串联:
|
模块 |
技术 |
运行位置 |
职责 |
|---|---|---|---|
|
ASR (语音识别) |
Whisper.cpp |
本地运行 |
麦克风采集 → 实时转文本 |
|
LLM (大语言模型) |
Kimi API |
云端调用 |
文本 → 流式生成回复 |
核心价值:
-
隐私优先:语音数据本地处理,不上传云端
-
低延迟:本地 ASR 避免网络传输延迟
-
低成本:Whisper 本地免费运行,只消耗 LLM API 费用
典型应用场景
-
语音助手/智能客服原型
-
实时会议记录与 AI 摘要
-
无障碍辅助工具开发
-
本地隐私敏感的语音交互应用
1.2 项目结构一览
ai-sdk-cpp-laoliao/applications/voice_ai_chat/
├── main.cpp # 程序入口:参数解析、初始化、主循环
├── voice_ai_chat.h # 核心类定义与配置结构
├── voice_ai_chat.cpp # 核心实现:LLM 工作线程、队列管理
├── CMakeLists.txt # 编译配置
└── README.md # 项目文档
依赖关系:
main.cpp
├── voice_ai_chat.h/cpp (AI 对话层)
├── realtime_coordinator.h (ASR 协调层)
├── audio_capture.h (音频采集)
├── endpoint_detector.h (VAD 端点检测)
└── asr_worker.h (Whisper 识别工作线程)
项目源码领取:全网首发!C++实现Whisper+Kimi端到端AI智能语音助手
1.3 编译与运行
编译步骤
# 1. 确保依赖已安装
sudo apt-get install -y libsdl2-dev pkg-config # Ubuntu/Debian
# 或
brew install sdl2 pkg-config # macOS
# 2. 进入项目目录
cd ai-sdk-cpp-laoliao
# 3. 创建构建目录
mkdir -p build && cd build
# 4. 编译
cmake ..
make -j$(nproc) voice_ai_chat
运行前的必要配置
必须设置环境变量:
export MOONSHOT_API_KEY="your-moonshot-api-key-here"
获取方式:访问 Moonshot AI 开放平台 注册并创建 API Key。
可选环境变量:
export WHISPER_MODEL_PATH="/path/to/your/model.bin" # 指定模型路径
export VOICE_AI_LOG_LEVEL=debug # 设置日志级别
1.4 运行参数详解
基础运行命令
# 方式1:纯文本模式(调试 LLM 链路)
./applications/bin/voice_ai_chat --stdin
# 方式2:语音模式(指定模型路径)
./applications/bin/voice_ai_chat ../../../models/ggml-small.bin
# 方式3:语音模式 + 完整参数
./applications/bin/voice_ai_chat \
../../../models/ggml-small.bin \
--mode balanced \
--vad-threshold 0.06 \
--threads 8
参数分类速查表
A. 模式选择参数
|
参数 |
说明 |
可选值 |
默认值 |
|---|---|---|---|
|
--stdin |
文本输入模式(不走语音) |
- |
关闭 |
|
--mode |
ASR 速度/质量模式 |
fast/balanced/quality |
balanced |
B. 模型与路径参数
|
参数 |
说明 |
示例 |
|---|---|---|
|
model_path |
Whisper 模型文件路径(位置参数) |
../../../models/ggml-small.bin |
|
--ai-model |
大模型名称 |
kimi-k2.5, kimi-k1.5 |
C. VAD (语音检测) 参数
|
参数 |
说明 |
建议范围 |
默认值 |
|---|---|---|---|
|
--vad-threshold |
语音检测阈值(越小越灵敏) |
0.03 ~ 0.15 |
0.06 |
|
--poll-interval-ms |
VAD 检测间隔(毫秒) |
100 ~ 500 |
250 |
|
--max-segment-ms |
单次送识别最大音频长度 |
1000 ~ 5000 |
2200 |
D. 性能参数
|
参数 |
说明 |
建议范围 |
默认值 |
|---|---|---|---|
|
--threads |
ASR 推理线程数 |
4 ~ 16 |
8 |
|
--llm-queue-size |
LLM 请求队列长度 |
4 ~ 16 |
8 |
E. 文本优化参数
|
参数 |
说明 |
示例 |
|---|---|---|
|
--prompt-file |
加载自定义提示词文件 |
--prompt-file ../prompts/tech.txt |
|
--hotwords-file |
加载热词列表 |
--hotwords-file ../hotwords/tech.txt |
|
--lexicon-file |
加载词库纠错 |
--lexicon-file ../lexicons/common.txt |
|
--replacements-file |
加载术语替换规则 |
--replacements-file ../replacements/tech.txt |
F. Partial 预览参数(建议关闭)
|
参数 |
说明 |
建议范围 |
默认值 |
|---|---|---|---|
|
--enable-partial |
开启说话过程中的临时预览 |
- |
关闭 |
|
--partial-interval-ms |
partial 刷新周期 |
400 ~ 700 |
700 |
|
--partial-min-segment-ms |
partial 最短音频 |
500 ~ 800 |
900 |
|
--partial-max-segment-ms |
partial 最长音频 |
1200 ~ 2200 |
1600 |
G. 设备与日志参数
|
参数 |
说明 |
|---|---|
|
--list-input-devices |
列出可用输入设备并退出 |
|
--input-device |
指定输入设备(名称或索引) |
|
--log-level |
日志级别:debug/info/warn/error |
1.5 模型选择与推理速度
Whisper 模型对比
Whisper.cpp 支持多种模型,模型越大准确率越高,但速度越慢。
|
模型 |
参数量 |
文件大小 |
实时因子 (RTF) |
适用场景 |
推荐指数 |
|---|---|---|---|---|---|
|
tiny |
39M |
~75MB |
~0.1x |
极简设备、高实时要求 |
⭐⭐⭐ |
|
base |
74M |
~142MB |
~0.3x |
平衡选择、移动端 |
⭐⭐⭐⭐ |
|
small |
244M |
~466MB |
~1.0x |
推荐默认、PC端实时 |
⭐⭐⭐⭐⭐ |
|
medium |
769M |
~1.5GB |
~3-5x |
质量优先、短句 |
⭐⭐⭐⭐ |
|
large-v3 |
1.5B |
~2.9GB |
~8-15x |
非实时、高质量需求 |
⭐⭐ |
RTF (Real-Time Factor): 处理1秒音频需要的实际秒数。RTF < 1 才能实时。
硬件配置建议
CPU 要求
|
模型 |
最低 CPU |
推荐 CPU |
内存需求 |
|---|---|---|---|
|
tiny |
2核 |
4核 |
2GB |
|
base |
2核 |
4核 |
4GB |
|
small |
4核 |
8核+ |
6GB |
|
medium |
8核 |
16核+ |
12GB |
|
large-v3 |
16核 |
32核+ |
16GB |
模式选择指南
根据你的 CPU 性能和实时性要求选择 --mode:
# 场景1:强实时、短句命令(如语音输入框)
--mode fast --threads 8 --max-segment-ms 1600
# 特点:延迟最低,准确率略低
# 场景2:普通对话、平衡选择(推荐)
--mode balanced --threads 8 --max-segment-ms 2200
# 特点:速度与质量兼顾
# 场景3:长句、质量优先(会议记录)
--mode quality --threads 8 --max-segment-ms 2600
# 特点:准确率最高,延迟较大
模式内部参数详解
|
模式 |
max_len |
max_tokens |
audio_ctx |
beam/best_of |
适用 |
|---|---|---|---|---|---|
|
fast |
32/40 |
24/32 |
256/384 |
best_of=1 |
实时命令 |
|
balanced |
32/72 |
24/0 |
256/512 |
best_of=2 |
日常对话 |
|
quality |
32/96 |
24/0 |
256/640 |
beam=4 |
精确转录 |
格式说明:partial参数/final参数。max_tokens=0 表示不限制。
1.6 推荐配置组合
配置 A:极速实时(短句命令)
./voice_ai_chat ../../../models/ggml-small.bin \
--mode fast \
--vad-threshold 0.05 \
--poll-interval-ms 200 \
--threads 8 \
--max-segment-ms 1600
适用:语音输入框、快速命令、即时响应场景
配置 B:平衡模式(推荐日常使用)
./voice_ai_chat ../../../models/ggml-small.bin \
--mode balanced \
--vad-threshold 0.06 \
--poll-interval-ms 250 \
--threads 8 \
--max-segment-ms 2200 \
--prompt-file ../prompts/general.txt \
--lexicon-file ../lexicons/common.txt
适用:日常对话、办公场景
配置 C:质量优先(会议记录)
./voice_ai_chat ../../../models/ggml-medium.bin \
--mode quality \
--vad-threshold 0.06 \
--poll-interval-ms 300 \
--threads 8 \
--max-segment-ms 2600 \
--prompt-file ../prompts/general.txt \
--replacements-file ../replacements/tech.txt
适用:会议记录、访谈转录、质量优先场景
配置 D:低资源设备
./voice_ai_chat ../../../models/ggml-base.bin \
--mode fast \
--vad-threshold 0.08 \
--poll-interval-ms 300 \
--threads 4 \
--max-segment-ms 1800
适用:嵌入式设备、旧电脑、树莓派等
1.7 常见问题诊断
Q1: 说话但没有文本输出?
排查步骤:
-
检查日志级别:--log-level debug
-
确认 VAD 是否检测到语音:看 [VAD-DEBUG] 日志
-
检查音频设备:--list-input-devices 确认使用正确设备
-
调整 VAD 阈值:--vad-threshold 0.05(更灵敏)
Q2: 推理速度太慢?
优化建议(按优先级):
-
换小模型:large → medium → small → base
-
用 fast 模式:--mode fast
-
缩短片段:--max-segment-ms 1600
-
增加线程:--threads 8(不超过物理核心数)
-
检查 CPU:是否在运行其他占用 CPU 的程序?
Q3: 出现很多繁体字?
解决方案:
-
使用 --prompt-file 加载简体提示词
-
使用 --lexicon-file 加载词库纠错
-
后续可考虑集成 OpenCC 进行繁转简
Q4: 输出中出现"常见术语包括"等 prompt 内容?
这是 prompt 泄漏,说明提示词太长。解决方法:
-
改用短提示词:../prompts/general.txt
-
停用 --hotwords-file,改用 --replacements-file
第2章 架构设计与数据流
2.1 整体架构概览
四层架构设计
┌─────────────────────────────────────────────────────────────────────┐
│ Voice AI Chat │
│ (应用层 / 协调层) │
├─────────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ASR层 │───▶│ Final文本 │───▶│ LLM层 │ │
│ │ (本地Whisper)│ │ 队列 │ │ (云端Kimi) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ │
│ │ VAD层 │───▶│ 端点检测器 │ │
│ │ (语音检测) │ │ │ │
│ └─────────────┘ └─────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ │
│ │ 采集层 │ SDL2 + 环形缓冲区 │
│ │ (麦克风) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
设计哲学:解耦与异步
为什么这样分层?
|
旧版问题 |
当前解决方案 |
|---|---|
|
VAD、分段、ASR 都在主循环 |
各层独立,通过队列通信 |
|
ASR 阻塞导致音频丢失 |
ASR 在独立 Worker 线程 |
|
无法扩展 partial/final 双通道 |
回调机制便于扩展 |
|
难接入 LLM(网络阻塞) |
独立 LLM Worker 线程 |
2.2 数据流详解
阶段1:音频采集层
// AudioCapture 核心职责
class AudioCapture {
// 1. SDL2 初始化音频设备
// 2. 音频回调写入环形缓冲区
// 3. 提供 GetAudioRange() 供 VAD 读取
};
关键设计:
-
Ring Buffer:避免内存分配,支持实时读取
-
绝对时间戳:每个采样都有全局时间,便于切片
阶段2:VAD 端点检测层
// EndpointDetector 核心职责
class EndpointDetector {
// 1. 定时从 AudioCapture 读取音频窗口
// 2. 调用 whisper_vad_segments() 检测语音概率
// 3. 判断语音开始/结束,生成 utterance 区间
};
状态流转:
静音状态 ──(vad_prob > threshold)──▶ 说话中 ──(vad_prob < threshold 持续 N ms)──▶ 静音状态
│ │
▼ ▼
触发 "speech started" 触发 "speech ended"
生成 utterance
阶段3:ASR 识别层
// AsrWorker 核心职责(独立线程)
class AsrWorker {
// 1. 从队列取出 utterance job
// 2. 调用 whisper_full_with_state() 识别
// 3. 输出识别结果(partial / final)
};
背压控制:
utterance queue (有界队列)
┌──────────────────────────────────┐
│ [job1] [job2] [job3] ... │
└──────────────────────────────────┘
▲ ▼
EndpointDetector AsrWorker
(生产者) (消费者)
队列满时的策略:丢弃最旧的 job,保证实时性
阶段4:LLM 对话层
// VoiceAIChat 核心职责(独立线程)
class VoiceAIChat {
// 1. 通过回调接收 final 文本
// 2. LLM Worker 线程消费文本队列
// 3. 流式调用 Kimi API
// 4. 管理对话历史
};
数据流向:
ASR final output
│
▼
┌──────────────────┐
│ final_text_queue │ (双端队列,mutex 保护)
└──────────────────┘
│
▼
┌──────────────────┐
│ LLM Worker Loop │ (condition_variable 等待)
└──────────────────┘
│
▼
┌──────────────────┐
│ Kimi stream_text │ (HTTP SSE 流式)
└──────────────────┘
│
▼
控制台输出
2.3 时序图
完整交互时序

2.4 线程模型
线程分布图
主线程 (main)
├── AudioCapture 线程 (SDL2 音频回调线程)
├── AsrWorker 线程 (Whisper 推理)
├── LLM Worker 线程 (VoiceAIChat)
└── 主循环 (coordinator.Run())
线程职责与同步
|
线程 |
职责 |
同步机制 |
|---|---|---|
|
主线程 |
初始化、信号处理、协调器主循环 |
- |
|
SDL 音频线程 |
采集音频写入 ring buffer |
无锁 ring buffer |
|
AsrWorker |
消费 utterance 队列,执行识别 |
std::deque + std::mutex |
|
LLM Worker |
消费 final 文本队列,调用 API |
std::deque + std::mutex + std::condition_variable |
关键同步代码
// LLM Worker 等待队列(voice_ai_chat.cpp)
void VoiceAIChat::LLMWorkerLoop() {
while (true) {
std::string user_text;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
queue_cv_.wait(lock, [this]() {
return !running_.load() || !final_text_queue_.empty();
});
if (!running_.load() && final_text_queue_.empty()) {
return; // 退出信号
}
user_text = std::move(final_text_queue_.front());
final_text_queue_.pop_front();
} // 解锁
ProcessWithAI(user_text); // 处理请求
}
}
// 生产者入队
void VoiceAIChat::EnqueueFinalText(const std::string& text) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
if (final_text_queue_.size() >= config_.max_pending_final_texts) {
final_text_queue_.pop_front(); // 背压:丢弃最旧
}
final_text_queue_.push_back(text);
}
queue_cv_.notify_one(); // 通知消费者
}
2.5 为什么 Partial 不进 LLM?
Partial vs Final
|
特性 |
Partial |
Final |
|---|---|---|
|
触发时机 |
说话过程中定期输出 |
一句话说完后输出 |
|
稳定性 |
频繁变化、可能回滚 |
稳定、确定 |
|
用途 |
屏幕预览给用户看 |
正式送 LLM 处理 |
如果把 Partial 送进 LLM 会怎样?
用户说话:"今天天气怎么样"
时间线:
T1: partial -> "今天天" ──┐
T2: partial -> "今天天气怎" ──┼── 都送进 LLM?
T3: partial -> "今天天气怎么样" ──┘
T4: final -> "今天天气怎么样"
问题:
1. 上下文污染:LLM 看到 3 次重复请求
2. Token 浪费:多次请求消耗 API 额度
3. 过早触发:话没说完就生成回复
推荐策略
┌─────────────────────────────────────┐
│ ASR 层 (Realtime) │
│ ┌─────────┐ ┌─────────────┐ │
│ │ Partial │─────▶│ 屏幕显示 │ │ (不送 LLM)
│ └─────────┘ └─────────────┘ │
│ ┌─────────┐ ┌─────────────┐ │
│ │ Final │─────▶│ LLM Worker │ │ (正式处理)
│ └─────────┘ └─────────────┘ │
└─────────────────────────────────────┘
2.6 扩展设计:未来方向
阶段 2 规划
当前架构 目标架构
────────── ──────────
┌────────┐ ┌────────┐
│ ASR │ │ ASR │
│ small │ │ tiny/ │──▶ Partial (预览)
└────┬───┘ │ small │
│ └────┬───┘
▼ │
┌────────┐ ▼
│ LLM │ ┌────────┐
│ (Kimi) │ │ LLM │
└────────┘ │ (Kimi) │
└────────┘
新增:
• 双模型策略 (tiny + small)
• TTS 回播
• Barge-in (打断检测)
可能的架构演进
// 未来可能的多级架构
class VoiceAIChat {
// 层1:ASR (本地)
// 层2:理解/意图 (本地轻量模型或规则)
// 层3:LLM (云端或本地大模型)
// 层4:TTS (本地或云端)
};
2.7 本章小结
核心要点:
-
四层架构:采集 → VAD → ASR → LLM,各层解耦
-
异步设计:ASR 和 LLM 都有独立 Worker 线程
-
队列通信:生产者-消费者模式,有界队列背压
-
Partial 策略:仅用于预览,不进 LLM
关键代码路径:
-
数据流:AudioCapture → EndpointDetector → AsrWorker → VoiceAIChat → Kimi
-
同步点:std::mutex 保护队列,std::condition_variable 唤醒 Worker
思考题:
-
为什么要用双端队列 (std::deque) 而不是普通队列?
-
如果 LLM 响应很慢,如何设计才能不阻塞新的 ASR 结果?
-
如何实现 "Barge-in"(用户打断 AI 说话)功能?
第3章 核心模块源码解析
3.1 VoiceAIChat 类概览
类定义(voice_ai_chat.h)
namespace voice_ai {
struct VoiceAIConfig {
std::string moonshot_base_url = "https://api.moonshot.cn";
std::string ai_model = "kimi-k2.5";
std::string ai_system_prompt = "你是 Kimi,一个中文优先...";
std::string moonshot_api_key;
bool enable_streaming = true;
size_t max_pending_final_texts = 8;
realtime::LogLevel log_level = realtime::LogLevel::kInfo;
};
class VoiceAIChat {
public:
explicit VoiceAIChat(const VoiceAIConfig& config);
~VoiceAIChat();
// 禁止拷贝(资源管理语义)
VoiceAIChat(const VoiceAIChat&) = delete;
VoiceAIChat& operator=(const VoiceAIChat&) = delete;
// 生命周期
bool Init();
void Stop();
// 回调设置(用于外部集成)
void SetTranscriptionCallback(TranscriptionCallback callback);
void SetAIResponseCallback(AIResponseCallback callback);
// 输入接口
void SendTextMessage(const std::string& text); // 文本模式
void HandleFinalTranscript(const std::string& text); // ASR 回调
private:
// 内部实现...
};
} // namespace voice_ai
设计模式分析
|
设计要点 |
实现方式 |
目的 |
|---|---|---|
|
配置集中 |
VoiceAIConfig 结构体 |
参数统一管理 |
|
资源安全 |
= delete 拷贝构造 |
防止资源重复释放 |
|
延迟初始化 |
std::optional<ai::Client> |
延迟创建 API 客户端 |
|
回调机制 |
std::function |
支持外部扩展 |
3.2 初始化流程
Init() 方法
bool VoiceAIChat::Init() {
if (!InitAIClient()) { // 步骤1:初始化 AI 客户端
return false;
}
StartLLMWorker(); // 步骤2:启动工作线程
return true;
}
InitAIClient() 详解
bool VoiceAIChat::InitAIClient() {
// 设置日志(只显示警告和错误)
ai::logger::install_logger(
std::make_shared<ai::logger::ConsoleLogger>(
ai::logger::LogLevel::kLogLevelWarn));
// 获取 API Key(环境变量优先)
const auto api_key = config_.moonshot_api_key.empty()
? GetEnvOrDefault("MOONSHOT_API_KEY", "")
: config_.moonshot_api_key;
if (api_key.empty()) {
realtime::AppLogger::Instance().Error(
"MOONSHOT_API_KEY environment variable not set");
return false;
}
// 创建客户端(使用 std::optional 延迟初始化)
ai_client_.emplace(
ai::openai::create_client(api_key, config_.moonshot_base_url));
if (!ai_client_->is_valid()) {
realtime::AppLogger::Instance().Error("AI client init failed");
return false;
}
// 初始化对话历史(系统提示词)
conversation_history_ = {
ai::Message::system(config_.ai_system_prompt),
};
return true;
}
关键点:
-
环境变量 MOONSHOT_API_KEY 必须设置
-
std::optional 实现延迟初始化,避免构造时出错
-
系统提示词在初始化时加入对话历史
3.3 LLM Worker 线程
线程生命周期
void VoiceAIChat::StartLLMWorker() {
bool expected = false;
// compare_exchange_strong: 原子 CAS 操作
// 确保只有一个线程能成功启动
if (!running_.compare_exchange_strong(expected, true)) {
return; // 已经在运行
}
llm_thread_ = std::thread(&VoiceAIChat::LLMWorkerLoop, this);
}
void VoiceAIChat::StopLLMWorker() {
const bool was_running = running_.exchange(false);
queue_cv_.notify_all(); // 唤醒所有等待的线程
if (was_running && llm_thread_.joinable()) {
llm_thread_.join(); // 等待线程结束
}
}
Worker 主循环(生产者-消费者模式)
void VoiceAIChat::LLMWorkerLoop() {
while (true) {
std::string user_text;
size_t pending_after_pop = 0;
{
// 1. 获取锁
std::unique_lock<std::mutex> lock(queue_mutex_);
// 2. 条件等待(原子释放锁并等待)
queue_cv_.wait(lock, [this]() {
// 唤醒条件:停止信号 或 队列非空
return !running_.load() || !final_text_queue_.empty();
});
// 3. 检查退出条件
if (!running_.load() && final_text_queue_.empty()) {
return; // 优雅退出
}
// 4. 消费消息
user_text = std::move(final_text_queue_.front());
final_text_queue_.pop_front();
pending_after_pop = final_text_queue_.size();
} // 5. 自动解锁
// 6. 处理(在锁外执行,避免阻塞入队)
ai_processing_ = true;
ProcessWithAI(user_text);
ai_processing_ = false;
}
}
入队方法(生产者)
void VoiceAIChat::EnqueueFinalText(const std::string& text) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
// 背压控制:队列满时丢弃最旧的消息
if (final_text_queue_.size() >= config_.max_pending_final_texts) {
realtime::AppLogger::Instance().Warn(
"final 文本队列已满,丢弃最旧的一条");
final_text_queue_.pop_front();
}
final_text_queue_.push_back(text);
} // 锁释放
queue_cv_.notify_one(); // 通知一个等待的消费者
}
为什么用 notify_one 而不是 notify_all?
-
只有一个 LLM Worker 线程
-
notify_one 更高效(只唤醒一个线程)
3.4 调用 Kimi API
ProcessWithAI() 主流程
void VoiceAIChat::ProcessWithAI(const std::string& user_text) {
// 1. 检查客户端状态
if (!ai_client_.has_value()) {
return;
}
// 2. 添加到对话历史(User 消息)
{
std::lock_guard<std::mutex> lock(mutex_);
conversation_history_.push_back(ai::Message::user(user_text));
}
// 3. 准备请求参数
ai::GenerateOptions generate_options;
{
std::lock_guard<std::mutex> lock(mutex_);
generate_options.model = config_.ai_model;
generate_options.messages = conversation_history_;
}
// 4. 打印 AI 前缀(带颜色)
{
std::lock_guard<std::mutex> lock(output_mutex_);
std::cout << kAnsiAssistantColor << "Kimi> " << std::flush;
}
// 5. 流式调用
if (config_.enable_streaming) {
ProcessStreaming(generate_options);
} else {
ProcessNonStreaming(generate_options);
}
}
流式处理详解
void VoiceAIChat::ProcessStreaming(ai::GenerateOptions& options) {
ai::StreamOptions stream_options(std::move(options));
auto stream = ai_client_->stream_text(stream_options);
std::string assistant_reply;
bool stream_failed = false;
for (const auto& event : stream) {
if (event.is_text_delta()) {
// 收到文本片段
assistant_reply += event.text_delta;
// 外部回调
if (ai_response_callback_) {
ai_response_callback_(event.text_delta, true);
}
// 实时输出
std::lock_guard<std::mutex> lock(output_mutex_);
std::cout << event.text_delta << std::flush;
} else if (event.is_error()) {
// 流错误处理
stream_failed = true;
realtime::AppLogger::Instance().Error(
"Kimi stream error: " + event.error.value_or("unknown"));
break;
}
}
// 完成处理
std::lock_guard<std::mutex> lock(mutex_);
if (stream_failed || assistant_reply.empty()) {
// 失败时回退 User 消息(对话不保存)
conversation_history_.pop_back();
} else {
// 成功,添加 Assistant 回复到历史
conversation_history_.push_back(
ai::Message::assistant(assistant_reply));
}
}
流式响应的优势:
-
用户体验好:边生成边显示
-
感知延迟低:不需要等全部生成完
-
与 ChatGPT 等产品体验一致
3.5 主程序入口
main() 函数结构(main.cpp)
int main(int argc, char** argv) {
try {
// 1. 配置解析
voice_ai::VoiceAIConfig config;
realtime::RuntimeOptions realtime_options;
ParseArguments(argc, argv, config, realtime_options);
// 2. 环境变量覆盖
ApplyEnvironmentVariables(config, realtime_options);
// 3. 加载外部文件(提示词、词库等)
LoadExternalFiles(realtime_options);
// 4. 初始化日志
realtime::AppLogger::Instance().SetLevel(config.log_level);
// 5. 列出设备(如果指定)
if (realtime_options.list_input_devices) {
return realtime::AudioCapture::ListInputDevices() ? 0 : 1;
}
// 6. 创建应用实例
voice_ai::VoiceAIChat app(config);
// 7. 信号处理(Ctrl+C 优雅退出)
std::signal(SIGINT, HandleSignal);
if (!app.Init()) {
return 1;
}
// 8. 模式选择:文本模式 或 语音模式
if (stdin_mode) {
RunStdinMode(app);
} else {
RunVoiceMode(app, realtime_options);
}
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
return 1;
}
}
语音模式初始化
void RunVoiceMode(voice_ai::VoiceAIChat& app,
const realtime::RuntimeOptions& options) {
// 1. 创建协调器
realtime::RealtimeCoordinator coordinator(options);
// 2. 设置 ASR final 回调(核心集成点)
coordinator.SetFinalTextCallback(
[&app](const std::string& text) {
app.HandleFinalTranscript(text);
});
// 3. 初始化并运行
if (!coordinator.Init()) {
std::cerr << "[MAIN] realtime coordinator init failed\n";
return 1;
}
coordinator.Run(); // 阻塞直到停止
}
核心集成点:SetFinalTextCallback 将 ASR 层和 LLM 层连接起来。
3.6 参数解析详解
命令行解析逻辑
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--stdin") {
stdin_mode = true;
}
else if (arg == "--ai-model" && i + 1 < argc) {
config.ai_model = argv[++i]; // 后置递增,先取值再移动
}
else if (arg == "--llm-queue-size" && i + 1 < argc) {
config.max_pending_final_texts =
static_cast<size_t>(std::stoul(argv[++i]));
}
// ... 更多参数
else if (!arg.empty() && arg[0] != '-') {
cli_model_path = arg; // 位置参数:模型路径
}
}
环境变量优先级
// 环境变量优先级高于命令行
if (const char* env = std::getenv("WHISPER_MODEL_PATH");
env != nullptr && !std::string(env).empty()) {
realtime_options.model_path = env;
} else if (!cli_model_path.empty()) {
realtime_options.model_path = cli_model_path;
}
// 日志级别
if (const char* env = std::getenv("VOICE_AI_LOG_LEVEL"); env != nullptr) {
config.log_level = realtime::AppLogger::ParseLevel(env);
}
优先级规则:
-
环境变量(最高优先级)
-
命令行参数
-
默认值
3.7 信号处理与优雅退出
全局指针(用于信号处理)
namespace {
voice_ai::VoiceAIChat* g_app = nullptr;
realtime::RealtimeCoordinator* g_coordinator = nullptr;
}
信号处理器
void HandleSignal(int sig) {
if (sig == SIGINT) { // Ctrl+C
std::cout << "\n[MAIN] stopping...\n";
// 按依赖顺序停止
if (g_coordinator != nullptr) {
g_coordinator->Stop(); // 先停采集和 ASR
}
if (g_app != nullptr) {
g_app->Stop(); // 再停 LLM(完成正在进行的请求)
}
}
}
注册信号处理
int main(...) {
// ...
voice_ai::VoiceAIChat app(config);
g_app = &app;
std::signal(SIGINT, HandleSignal); // 注册处理器
// ...
}
为什么先停 Coordinator 再停 VoiceAIChat?
-
Coordinator 停止后不再产生新的 final 文本
-
VoiceAIChat 可以继续处理队列中的剩余请求
-
避免请求丢失
第4章 关键技术点剖析
4.1 线程安全设计
多线程访问的共享资源
class VoiceAIChat {
// 需要同步保护的成员
std::deque<std::string> final_text_queue_; // LLM 队列
ai::Messages conversation_history_; // 对话历史
std::optional<ai::Client> ai_client_; // API 客户端
// 同步原语
std::mutex mutex_; // 保护 conversation_history_
std::mutex queue_mutex_; // 保护 final_text_queue_
std::mutex output_mutex_; // 保护控制台输出顺序
std::condition_variable queue_cv_; // 队列非空通知
};
锁粒度设计
细粒度锁策略:
// Good: 不同资源用不同锁
std::mutex queue_mutex_; // 只保护队列
std::mutex history_mutex_; // 只保护对话历史
std::mutex output_mutex_; // 只保护输出
// Bad: 一把大锁保护所有
std::mutex big_lock_; // 避免!会造成不必要的阻塞
代码示例:
void VoiceAIChat::ProcessWithAI(const std::string& user_text) {
// 操作1:修改对话历史(需要锁)
{
std::lock_guard<std::mutex> lock(mutex_);
conversation_history_.push_back(ai::Message::user(user_text));
} // 锁立即释放
// 操作2:准备请求参数(不需要锁,因为 conversation_history_ 已复制)
ai::GenerateOptions generate_options;
{
std::lock_guard<std::mutex> lock(mutex_);
generate_options.messages = conversation_history_;
} // 锁立即释放
// 操作3:网络请求(不需要锁,避免阻塞其他线程)
auto stream = ai_client_->stream_text(stream_options);
// 操作4:输出响应(需要 output_mutex_ 保证输出顺序)
for (const auto& event : stream) {
std::lock_guard<std::mutex> lock(output_mutex_);
std::cout << event.text_delta << std::flush;
}
}
死锁避免
原则:
-
锁顺序一致:多个锁时,始终以相同顺序获取
-
避免锁嵌套:尽量使用独立锁,减少嵌套
-
使用 std::lock_guard:RAII 自动释放,避免忘记解锁
// 安全的嵌套锁(如果需要)
void SomeFunction() {
std::lock_guard<std::mutex> lock_a(mutex_a_);
{
std::lock_guard<std::mutex> lock_b(mutex_b_);
// 操作
} // lock_b 先释放
} // lock_a 后释放
// 危险:不同函数锁顺序不一致会导致死锁
void FunctionA() {
std::lock_guard<std::mutex> a(mutex_a_);
std::lock_guard<std::mutex> b(mutex_b_); // 顺序:a -> b
}
void FunctionB() {
std::lock_guard<std::mutex> b(mutex_b_); // 顺序:b -> a ❌
std::lock_guard<std::mutex> a(mutex_a_);
}
4.2 背压控制 (Backpressure)
问题场景
用户快速说话 ASR 识别 LLM 处理
│ │ │
▼ ▼ ▼
句子1 ──────────▶ 文本1 ────────▶ 请求1 (慢)
句子2 ──────────▶ 文本2 ──────▶ 请求2 (等待中)
句子3 ──────────▶ 文本3 ──▶ 请求3 (堆积...)
句子4 ──────────▶ 文本4
...
问题:LLM 处理慢,队列无限增长 → 内存溢出、延迟累积
解决方案:有界队列 + 丢弃策略
void VoiceAIChat::EnqueueFinalText(const std::string& text) {
std::lock_guard<std::mutex> lock(queue_mutex_);
// 背压控制:队列满时丢弃最旧的消息
if (final_text_queue_.size() >= config_.max_pending_final_texts) {
realtime::AppLogger::Instance().Warn(
"final 文本队列已满,丢弃最旧的一条");
final_text_queue_.pop_front(); // 丢弃最旧
}
final_text_queue_.push_back(text); // 加入最新
}
背压策略对比
|
策略 |
实现 |
适用场景 |
|---|---|---|
|
丢弃最旧 |
pop_front() + push_back() |
实时性优先(本项目使用) |
|
丢弃最新 |
拒绝入队 |
完整性优先 |
|
阻塞入队 |
队列满时阻塞生产者 |
吞吐优先 |
|
动态扩容 |
队列自动增长 |
内存充足场景 |
为什么选"丢弃最旧"?
-
语音对话场景:用户更关心最新说的话
-
旧消息可能已经"过时"(上下文已变)
-
避免延迟累积,保证实时响应
4.3 回调机制设计
回调类型定义
// ASR 结果回调
using TranscriptionCallback =
std::function<void(const std::string& text, bool is_final)>;
// AI 响应回调
using AIResponseCallback =
std::function<void(const std::string& text, bool is_streaming_chunk)>;
回调注册与调用
class VoiceAIChat {
public:
// 设置回调(支持外部扩展)
void SetTranscriptionCallback(TranscriptionCallback callback) {
transcription_callback_ = std::move(callback);
}
void SetAIResponseCallback(AIResponseCallback callback) {
ai_response_callback_ = std::move(callback);
}
private:
void HandleTranscription(const std::string& text, bool is_final) {
// 调用外部注册的回调
if (transcription_callback_) {
transcription_callback_(text, is_final);
}
}
TranscriptionCallback transcription_callback_;
AIResponseCallback ai_response_callback_;
};
使用示例
// 主程序中注册回调
voice_ai::VoiceAIChat app(config);
// 回调1:收到 ASR 结果时打印日志
app.SetTranscriptionCallback(
[](const std::string& text, bool is_final) {
if (is_final) {
LOG_INFO << "Final transcription: " << text;
}
});
// 回调2:收到 AI 响应时发送到 WebSocket
app.SetAIResponseCallback(
[&websocket](const std::string& chunk, bool is_streaming) {
websocket.Send(chunk); // 实时推送到前端
});
Lambda 捕获注意事项
// 危险:捕获局部变量的引用
void Dangerous() {
voice_ai::VoiceAIChat app(config);
std::string prefix = "User: ";
app.SetTranscriptionCallback(
[&prefix](const std::string& text, bool) { // ❌ 引用捕获
std::cout << prefix << text << "\n"; // Dangerous 返回后 prefix 失效
});
} // prefix 被销毁,回调变成悬空引用
// 安全:值捕获 或 捕获指针
void Safe() {
auto app = std::make_shared<voice_ai::VoiceAIChat>(config);
std::string prefix = "User: ";
app->SetTranscriptionCallback(
[prefix, app](const std::string& text, bool) { // ✅ 值捕获
std::cout << prefix << text << "\n"; // 安全,有独立的拷贝
});
}
4.4 条件变量使用模式
生产者-消费者完整模式
template<typename T>
class BoundedQueue {
public:
explicit BoundedQueue(size_t capacity) : capacity_(capacity) {}
// 生产者
void Push(T item) {
std::unique_lock<std::mutex> lock(mutex_);
// 等待队列有空位
not_full_.wait(lock, [this] {
return queue_.size() < capacity_;
});
queue_.push_back(std::move(item));
not_empty_.notify_one(); // 通知消费者
}
// 消费者
T Pop() {
std::unique_lock<std::mutex> lock(mutex_);
// 等待队列有数据
not_empty_.wait(lock, [this] {
return !queue_.empty();
});
T item = std::move(queue_.front());
queue_.pop_front();
not_full_.notify_one(); // 通知生产者
return item;
}
private:
std::deque<T> queue_;
size_t capacity_;
std::mutex mutex_;
std::condition_variable not_empty_;
std::condition_variable not_full_;
};
本项目中的变体(无界队列 + 丢弃)
void VoiceAIChat::LLMWorkerLoop() {
while (true) {
std::string user_text;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
// 条件等待:停止信号 或 队列非空
queue_cv_.wait(lock, [this] {
return !running_.load() || !final_text_queue_.empty();
});
// 检查退出条件
if (!running_.load() && final_text_queue_.empty()) {
return; // 优雅退出
}
// 消费
user_text = std::move(final_text_queue_.front());
final_text_queue_.pop_front();
} // 解锁
ProcessWithAI(user_text); // 处理(在锁外)
}
}
关键点:
-
wait() 会自动释放锁并阻塞,被唤醒时重新获取锁
-
使用 lambda 谓词防止虚假唤醒 (spurious wakeup)
-
处理逻辑在锁外执行,减少临界区
4.5 流式 HTTP 处理
SSE (Server-Sent Events) 基础
HTTP 请求
│
▼
POST /v1/chat/completions
Headers:
Authorization: Bearer {api_key}
Accept: text/event-stream ← 关键:请求流式响应
响应(流式):
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"choices":[{"delta":{"content":" world"}}]}
data: {"choices":[{"delta":{"content":"!"}}]}
data: [DONE]
流式处理代码
void VoiceAIChat::ProcessStreaming(ai::GenerateOptions& options) {
// 1. 创建流式选项
ai::StreamOptions stream_options(std::move(options));
// 2. 发起流式请求
auto stream = ai_client_->stream_text(stream_options);
// 3. 逐块处理
for (const auto& event : stream) {
if (event.is_text_delta()) {
// 收到文本片段
const std::string& chunk = event.text_delta;
// 实时输出
std::cout << chunk << std::flush;
// 累加完整回复
assistant_reply += chunk;
} else if (event.is_error()) {
// 错误处理
HandleError(event.error);
stream_failed = true;
break;
}
}
// 4. 流结束,更新对话历史
if (!stream_failed) {
conversation_history_.push_back(
ai::Message::assistant(assistant_reply));
}
}
流式 vs 非流式对比
|
特性 |
流式 (streaming) |
非流式 (blocking) |
|---|---|---|
|
首字延迟 |
低(毫秒级) |
高(等待全部生成) |
|
用户体验 |
边生成边看 |
等待后一次性显示 |
|
实现复杂度 |
高(需处理分片) |
低(一次请求响应) |
|
取消支持 |
可中断 |
需等待完成 |
|
适用场景 |
对话、长文本 |
短文本、简单查询 |
4.6 配置加载优先级
三级配置体系
┌─────────────────────────────────────────┐
│ Level 1: 代码硬编码默认值 │
│ (最低优先级) │
├─────────────────────────────────────────┤
│ Level 2: 命令行参数 │
│ (./voice_ai_chat --mode fast ...) │
├─────────────────────────────────────────┤
│ Level 3: 环境变量 │
│ (export WHISPER_MODEL_PATH=...) │
│ (最高优先级) │
└─────────────────────────────────────────┘
实现代码
// 优先级:环境变量 > 命令行 > 默认值
std::string GetModelPath(const std::string& cli_path) {
// 1. 检查环境变量(最高优先级)
if (const char* env = std::getenv("WHISPER_MODEL_PATH")) {
if (!std::string(env).empty()) {
return env;
}
}
// 2. 使用命令行参数
if (!cli_path.empty()) {
return cli_path;
}
// 3. 使用默认值
return "../../../models/ggml-small.bin";
}
// 应用示例
realtime_options.model_path = GetModelPath(cli_model_path);
为什么要这样设计?
|
配置来源 |
适用场景 |
优势 |
|---|---|---|
|
环境变量 |
敏感信息(API Key)、全局默认 |
不暴露在命令行历史,便于容器化部署 |
|
命令行 |
临时调整、脚本调用 |
灵活、可见、易于文档化 |
|
默认值 |
快速开始、示例 |
零配置启动 |
4.7 日志系统设计
日志级别
enum class LogLevel {
kDebug, // 详细调试信息(开发用)
kInfo, // 一般信息(运行状态)
kWarn, // 警告(需要注意但非致命)
kError // 错误(功能受影响)
};
日志使用示例
// 不同级别的日志
AppLogger::Instance().Debug("queue size=" + std::to_string(size));
AppLogger::Instance().Info("AI client initialized");
AppLogger::Instance().Warn("队列已满,丢弃旧消息");
AppLogger::Instance().Error("API request failed: " + error);
运行时调整日志级别
# 方式1:命令行
./voice_ai_chat --log-level debug
# 方式2:环境变量
export VOICE_AI_LOG_LEVEL=debug
./voice_ai_chat
生产环境建议
|
环境 |
推荐级别 |
原因 |
|---|---|---|
|
开发调试 |
debug |
查看详细数据流 |
|
测试环境 |
info |
关注运行状态 |
|
生产环境 |
warn |
减少日志量,只关注问题 |
第5章 实验演示
5.1 环境准备
检查清单
|
项目 |
检查命令 |
预期结果 |
|---|---|---|
|
API Key |
echo $MOONSHOT_API_KEY |
显示有效 key |
|
编译产物 |
ls build/applications/bin/voice_ai_chat |
文件存在 |
|
模型文件 |
ls models/ggml-small.bin |
文件存在 |
|
音频设备 |
arecord -l 或 ./voice_ai_chat --list-input-devices |
显示设备列表 |
快速编译脚本
#!/bin/bash
# build.sh - 一键编译脚本
cd ai-sdk-cpp-laoliao
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc) voice_ai_chat
echo "编译完成,二进制位置:"
echo " ./applications/bin/voice_ai_chat"
5.2 实验1:文本模式验证 LLM 链路
目的
在不涉及语音的情况下,先验证 AI 对话功能正常。
步骤
# 1. 设置 API Key
export MOONSHOT_API_KEY="your-api-key"
# 2. 启动文本模式
./applications/bin/voice_ai_chat --stdin
# 3. 输入测试
=== Voice AI Chat (stdin mode) ===
输入文本后回车,输入 /exit 退出。
你好,请介绍一下你自己
Kimi> 你好!我是 Kimi,一个 AI 助手...
今天的天气怎么样?
Kimi> 我无法获取实时天气信息...
/exit
预期输出
你(ASR)> 你好,请介绍一下你自己
Kimi> 你好!我是 Kimi,一个 AI 助手。我可以帮助你解答问题、写作...
你(ASR)> 今天的天气怎么样?
Kimi> 我无法获取实时天气信息...
故障排查
|
现象 |
可能原因 |
解决方案 |
|---|---|---|
|
MOONSHOT_API_KEY not set |
环境变量未设置 |
export MOONSHOT_API_KEY=xxx |
|
AI client init failed |
API Key 无效 |
检查 Key 是否正确 |
|
连接超时 |
网络问题 |
检查网络,或尝试 ping api.moonshot.cn |
|
返回乱码 |
终端编码 |
export LANG=en_US.UTF-8 |
5.3 实验2:基础语音模式
目的
验证完整的语音输入 → ASR → LLM 流程。
步骤
# 1. 列出设备(确认麦克风可用)
./applications/bin/voice_ai_chat --list-input-devices
# 预期输出:
# Input Device 0: HDA Intel PCH: ALC257 Analog (hw:0,0)
# Input Device 1: USB Audio Device: USB Audio (hw:1,0)
# 2. 基础语音模式(使用默认配置)
./applications/bin/voice_ai_chat \
../../../models/ggml-small.bin
# 3. 说话测试(建议中文短句)
# 示例:"你好"、"今天星期几"、"讲个笑话"
观察要点
-
VAD 检测:看到 [ENDPOINT] speech started 表示检测到语音开始
-
识别输出:看到 [TEXT] #1 xxx 表示 ASR 成功
-
LLM 响应:看到 Kimi> xxx 表示 AI 正在回复
预期完整输出
[VAD-DEBUG] max_prob=0.821, segments=1, speech=yes
[ENDPOINT] speech started
...
[ENDPOINT] speech ended
[QUEUE] enqueue utterance #1 start_ms=... end_ms=...
[ASR-WORKER] processing utterance #1 samples=...
[TEXT] #1 你好
你(ASR)> 你好
Kimi> 你好!很高兴见到你...
5.4 实验3:模型速度对比
目的
对比不同模型的推理速度,选择合适的模型。
测试脚本
#!/bin/bash
# benchmark.sh - 模型速度对比
MODELS=("tiny" "base" "small")
TEST_AUDIO="test.wav" # 准备一段 5 秒测试音频
echo "=== 模型速度对比测试 ==="
for model in "${MODELS[@]}"; do
echo ""
echo "测试模型: $model"
echo "模型大小: $(ls -lh models/ggml-$model.bin | awk '{print $5}')"
# 使用 time 统计时间
time ./applications/bin/voice_ai_chat \
models/ggml-$model.bin \
--mode fast \
--threads 8 \
2>&1 | grep -E "(real|user|sys|RTF)"
done
预期结果参考
|
模型 |
大小 |
理论 RTF |
实测延迟 |
|---|---|---|---|
|
tiny |
75MB |
~0.1x |
0.3-0.5s |
|
base |
142MB |
~0.3x |
0.8-1.2s |
|
small |
466MB |
~1.0x |
1.5-2.5s |
|
medium |
1.5GB |
~3-5x |
4-8s |
选择建议
# 低配设备(4核以下)
./voice_ai_chat models/ggml-base.bin --mode fast --threads 4
# 标准配置(8核,推荐)
./voice_ai_chat models/ggml-small.bin --mode balanced --threads 8
# 高配设备(追求质量)
./voice_ai_chat models/ggml-medium.bin --mode quality --threads 16
5.5 实验4:VAD 参数调优
目的
找到适合你麦克风和环境的 VAD 参数。
测试命令
# 测试1:高灵敏度(适合安静环境、远场语音)
./applications/bin/voice_ai_chat \
../../../models/ggml-small.bin \
--mode fast \
--vad-threshold 0.03 \
--poll-interval-ms 150
# 测试2:默认灵敏度
default: --vad-threshold 0.06 --poll-interval-ms 250
# 测试3:低灵敏度(适合嘈杂环境、近场语音)
./applications/bin/voice_ai_chat \
../../../models/ggml-small.bin \
--mode fast \
--vad-threshold 0.12 \
--poll-interval-ms 300
调优指南
|
场景 |
VAD 阈值 |
轮询间隔 |
原因 |
|---|---|---|---|
|
安静办公室 |
0.03-0.05 |
150-200ms |
灵敏检测,快速响应 |
|
普通家庭 |
0.06-0.08 |
250ms |
默认平衡 |
|
嘈杂环境 |
0.10-0.15 |
300-400ms |
减少误触发 |
|
近场麦克风 |
0.08-0.10 |
200-250ms |
避免过度灵敏 |
观察指标
# 好的 VAD 表现
[VAD-DEBUG] max_prob=0.921, speech=yes # 说话时有高概率
[ENDPOINT] speech started # 快速检测到开始
...
[VAD-DEBUG] max_prob=0.012, speech=no # 结束后低概率
[ENDPOINT] speech ended # 准确检测结束
# 不好的 VAD 表现(需要调参)
[VAD-DEBUG] max_prob=0.051, speech=no # 说话但检测为静音(阈值太高)
[VAD-DEBUG] max_prob=0.211, speech=yes # 环境噪音误触发(阈值太低)
5.6 实验5:模式参数对比
目的
理解 fast、balanced、quality 三种模式的区别。
测试方法
# 准备一段测试音频(建议 5-10 秒中文)
# 或者使用相同的一句话重复测试
# 测试 fast 模式
echo "=== Fast Mode ==="
time ./voice_ai_chat models/ggml-small.bin --mode fast \
--threads 8 2>&1 | grep -E "(real|TEXT)"
# 测试 balanced 模式
echo "=== Balanced Mode ==="
time ./voice_ai_chat models/ggml-small.bin --mode balanced \
--threads 8 2>&1 | grep -E "(real|TEXT)"
# 测试 quality 模式
echo "=== Quality Mode ==="
time ./voice_ai_chat models/ggml-small.bin --mode quality \
--threads 8 2>&1 | grep -E "(real|TEXT)"
参数对比实验
# 实验:max-segment-ms 对延迟的影响
for ms in 1600 2200 3000; do
echo "max-segment-ms = $ms"
time ./voice_ai_chat models/ggml-small.bin \
--mode balanced \
--max-segment-ms $ms \
--threads 8
done
结果分析
|
模式 |
准确率 |
延迟 |
适用场景 |
|---|---|---|---|
|
fast |
⭐⭐⭐ |
最低 |
命令词、短句 |
|
balanced |
⭐⭐⭐⭐ |
中等 |
日常对话 |
|
quality |
⭐⭐⭐⭐⭐ |
较高 |
长句、专业内容 |
5.7 实验6:文本优化功能
目的
测试热词、词库、替换规则对识别准确率的提升。
实验6.1:热词提示词
# 1. 创建热词文件
cat > hotwords_tech.txt << EOF
实时性
延迟
吞吐
模型推理
量化
神经网络
EOF
# 2. 使用热词运行
./voice_ai_chat models/ggml-small.bin \
--mode balanced \
--hotwords-file hotwords_tech.txt
# 3. 测试:说出这些技术术语,观察识别准确率
实验6.2:术语替换
# 1. 创建替换规则文件
cat > replacements_tech.txt << EOF
实实性 => 实时性
推力 => 推理
模形 => 模型
量化 => 量化
EOF
# 2. 使用替换规则运行
./voice_ai_chat models/ggml-small.bin \
--mode balanced \
--replacements-file replacements_tech.txt
# 3. 测试:故意说错词,观察是否被纠正
实验6.3:词库纠错
# 1. 创建词库文件
cat > lexicon_office.txt << EOF
# 标准词 <TAB> 别名1,别名2,...
带饭 带翻,代饭
报销 报消
工牌 工牌子
邮箱 油香
EOF
# 2. 使用词库运行
./voice_ai_chat models/ggml-small.bin \
--mode balanced \
--lexicon-file lexicon_office.txt
# 3. 测试:说出"带翻",观察输出是否为"带饭"
5.8 实验7:调试与日志
目的
学习使用日志排查问题。
开启详细日志
# debug 级别会显示所有内部状态
./voice_ai_chat models/ggml-small.bin \
--mode balanced \
--log-level debug \
2>&1 | tee voice_ai_debug.log
关键日志解读
# 过滤关键日志
# 1. VAD 检测
grep "VAD-DEBUG" voice_ai_debug.log
# 2. 端点事件
grep "ENDPOINT" voice_ai_debug.log
# 3. 队列操作
grep "QUEUE" voice_ai_debug.log
# 4. ASR 处理
grep "ASR-WORKER" voice_ai_debug.log
# 5. 最终文本
grep "TEXT" voice_ai_debug.log
常见问题日志模式
问题1:VAD 检测不到语音
# 现象:没有 speech started
[VAD-DEBUG] max_prob=0.021, segments=0, speech=no
# 解决:降低阈值 --vad-threshold 0.03
问题2:队列堆积
[QUEUE] enqueue utterance #10 ...
[QUEUE] skip utterance: too short ...
# 或
[QUEUE] skip utterance: empty audio range
# 解决:检查音频设备,或降低 max-segment-ms
问题3:LLM 队列满
[warn] final 文本队列已满,丢弃最旧的一条
# 解决:增加 --llm-queue-size,或检查网络
5.9 综合实验:配置你的最佳参数
实验目标
通过系统测试,找到适合你的硬件和场景的最佳参数组合。
测试矩阵
#!/bin/bash
# optimization.sh - 参数优化测试
MODELS=("base" "small")
MODES=("fast" "balanced")
THRESHOLDS=(0.04 0.06 0.08)
for model in "${MODELS[@]}"; do
for mode in "${MODES[@]}"; do
for threshold in "${THRESHOLDS[@]}"; do
echo ""
echo "=========================================="
echo "Model: $model, Mode: $mode, Threshold: $threshold"
echo "=========================================="
# 运行测试(建议录制一段标准测试语音)
timeout 30 ./voice_ai_chat \
models/ggml-$model.bin \
--mode $mode \
--vad-threshold $threshold \
--threads 8 \
--log-level info
done
done
done
评估维度
|
维度 |
评估方法 |
目标值 |
|---|---|---|
|
延迟 |
说完话到看到 Kimi> 的时间 |
< 3s |
|
准确率 |
正确识别字数 / 总字数 |
> 90% |
|
误触发 |
环境噪音触发次数 |
< 1次/分钟 |
|
丢字率 |
未检测到的说话次数 |
< 5% |
|
CPU 占用 |
top/htop 观察 |
< 70% |
学习资源推荐
Whisper.cpp 相关
TTS 相关
LLM 本地部署
C++ 并发编程
-
《C++ Concurrency in Action》(Anthony Williams)
-
cppreference.com 的并发部分
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)