1. 引言
在跨语言交流、会议记录、语音助手等场景中,实时语音翻译 是一项核心能力。我们希望用C++实现一个完整的端侧方案:麦克风采集英文语音,Vosk 进行离线语音识别,本地小模型 实时翻译为中文,并将中文结果即时打印出来。
本文详细阐述其系统设计、线程模型,并给出可直接编译运行的完整代码。翻译部分选用高效率的 CTranslate2 推理引擎加载 OPUS-MT 英→中模型,保证低延迟、高吞吐。
本文给出了一个完整的、基于 Vosk + CTranslate2 + ALSA 的实时语音翻译C++实现。通过生产者-消费者模型,将语音识别与翻译解耦,保证了音频流的低延迟处理。代码可直接编译运行,只需替换为你自己训练的本地小模型(或任何 CTranslate2 兼容模型),即可实现英文到中文的实时流式翻译。
2. 系统总体设计
系统由三个核心模块组成,运行于两个线程,通过线程安全的消息队列解耦:
-
音频采集与识别线程(主线程)
使用 ALSA 从麦克风读取 PCM 音频,喂入 Vosk 识别器,每当识别出一句完整的英文句子,就将原文推入队列。
-
翻译线程
从队列取出英文文本,调用本地翻译模型生成中文,并立刻输出到控制台。
-
翻译模型服务
基于 CTranslate2 加载预训练的英→中 Transformer 模型,搭配 SentencePiece 分词器完成文本到文本的转换。
这种"生产者-消费者"架构确保了音频采集不会被翻译的耗时操作阻塞,翻译也可独立优化,非常适合实时流式场景。
3. 关键技术选型
| 组件 | 作用 | 说明 |
|---|---|---|
| ALSA | 麦克风音频采集 | Linux 标准音频接口,提供低延迟 PCM 流 |
| Vosk | 离线英文语音识别 | 轻量级,支持多种语言,纯 C API,可完全本地运行 |
| CTranslate2 | 高效神经网络推理 | 针对 Transformer 模型优化,支持 CPU/GPU,比原生 PyTorch 快数倍 |
| OPUS-MT | 预训练英→中翻译模型 | 开源神经机器翻译模型,可转换为 CTranslate2 格式 |
| SentencePiece | 子词分词/解码 | 将文本拆分为模型可理解的 token,翻译后再还原为自然语言 |
| C++ 线程库 | 生产者-消费者、信号处理 | 使用 std::thread, std::mutex, std::condition_variable 等 |
4. 详细实现
4.1 音频采集与 Vosk 识别(主线程)
沿用你提供的框架,但改为加载英文模型 vosk-model-en-us-0.22。每次成功识别出完整句子(vosk_recognizer_accept_waveform 返回真),我们解析返回的 JSON,提取 "text" 字段,将英文原文放入全局队列。
cpp
// 从 Vosk JSON 结果中提取文本的辅助函数
std::string extract_text(const char* json) {
std::string s(json);
auto pos = s.find("\"text\" : \"");
if (pos == std::string::npos) return "";
pos += 10; // 跳过 "text" : "
auto end = s.find("\"", pos);
if (end == std::string::npos) return "";
return s.substr(pos, end - pos);
}
主循环将提取到的非空句子入队并唤醒翻译线程。
4.2 线程安全消息队列
使用 std::queue<std::string> 配合互斥锁和条件变量,实现阻塞式消费。
cpp
std::queue<std::string> message_queue;
std::mutex queue_mutex;
std::condition_variable queue_cv;
std::atomic<bool> running{true};
4.3 翻译线程与 CTranslate2 集成
这是本系统的核心。翻译线程启动后完成以下初始化:
- 加载源语言(英文)的 SentencePiece 模型,用于将英文句子切分成子词。
- 加载目标语言(中文)的 SentencePiece 模型,用于将翻译出的子词序列还原为中文。
- 创建 CTranslate2
Translator对象,指向转换后的模型目录。
然后进入循环,从队列获取句子,执行"分词 → 翻译 → 解码 → 输出"流水线。
4.4 信号处理
注册 SIGINT 和 SIGTERM 信号,以便用 Ctrl+C 安全退出程序:设置 running = false,唤醒所有线程,确保资源正确释放。
5. 完整C++代码
以下代码为完整实现,适当注释以便理解。请将模型路径和分词器路径修改为你本机的实际位置。
cpp
#include <alsa/asoundlib.h>
#include <vosk_api.h>
#include <ctranslate2/translator.h>
#include <sentencepiece/sentencepiece_processor.h>
#include <iostream>
#include <string>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <signal.h>
#include <cstring>
// ---------- 配置 ----------
#define SAMPLE_RATE 44100
#define BUFFER_FRAMES (SAMPLE_RATE * 2) // 每次读取约2秒音频
// Vosk 英文模型路径
#define VOSK_MODEL_PATH "vosk-model-en-us-0.22"
// CTranslate2 翻译模型路径(OPUS-MT英→中,已转换为CTranslate2格式)
#define CT2_MODEL_PATH "opus-mt-en-zh"
// SentencePiece 分词模型路径(需与翻译模型配套)
#define SRC_SP_MODEL "source.spm" // 英文分词
#define TGT_SP_MODEL "target.spm" // 中文分词/解码
// ---------- 线程安全队列 ----------
std::queue<std::string> message_queue;
std::mutex queue_mutex;
std::condition_variable queue_cv;
std::atomic<bool> running{true};
// ---------- 信号处理 ----------
void signal_handler(int) {
running = false;
queue_cv.notify_all();
}
// ---------- 从 Vosk JSON 提取 "text" 字段 ----------
std::string extract_text(const char* json) {
if (!json) return "";
std::string s(json);
auto pos = s.find("\"text\" : \"");
if (pos == std::string::npos) return "";
pos += 10; // 跳过标记
auto end = s.find("\"", pos);
if (end == std::string::npos) return "";
return s.substr(pos, end - pos);
}
// ---------- 翻译线程 ----------
void translator_thread() {
// 1. 初始化 SentencePiece 分词器
sentencepiece::SentencePieceProcessor src_sp, tgt_sp;
if (!src_sp.Load(SRC_SP_MODEL).ok() || !tgt_sp.Load(TGT_SP_MODEL).ok()) {
std::cerr << "Failed to load SentencePiece models!" << std::endl;
running = false;
return;
}
// 2. 初始化 CTranslate2 翻译器(CPU模式,可改为 CUDA)
ctranslate2::Translator translator(CT2_MODEL_PATH, ctranslate2::Device::CPU);
std::cout << "翻译模型加载完毕,开始实时翻译..." << std::endl;
// 3. 循环处理队列中的英文句子
while (running) {
std::string english;
{
std::unique_lock<std::mutex> lock(queue_mutex);
queue_cv.wait(lock, [] { return !message_queue.empty() || !running; });
if (!running && message_queue.empty()) break; // 退出
english = std::move(message_queue.front());
message_queue.pop();
}
if (english.empty()) continue;
// 分词(英文 → 子词序列)
std::vector<std::string> src_tokens;
src_sp.Encode(english, &src_tokens);
// 翻译(子词序列 → 目标子词序列)
ctranslate2::TranslationResult result;
try {
result = translator.translate_batch({src_tokens})[0];
} catch (const std::exception& e) {
std::cerr << "翻译失败: " << e.what() << std::endl;
continue;
}
// 解码(目标子词序列 → 中文文本)
std::string chinese;
tgt_sp.Decode(result.output(), &chinese); // output() 返回 vector<string>
// 实时输出中文
std::cout << chinese << std::endl;
}
}
// ---------- 主函数 ----------
int main() {
// 注册信号
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// 1. 加载 Vosk 英文模型
VoskModel* model = vosk_model_new(VOSK_MODEL_PATH);
if (!model) {
std::cerr << "无法加载 Vosk 模型: " << VOSK_MODEL_PATH << std::endl;
return 1;
}
VoskRecognizer* rec = vosk_recognizer_new(model, SAMPLE_RATE);
if (!rec) {
std::cerr << "无法创建识别器" << std::endl;
vosk_model_free(model);
return 1;
}
// 2. 打开 ALSA 录音设备
snd_pcm_t* pcm;
int ret = snd_pcm_open(&pcm, "default", SND_PCM_STREAM_CAPTURE, 0);
if (ret < 0) {
std::cerr << "无法打开音频设备: " << snd_strerror(ret) << std::endl;
vosk_recognizer_free(rec);
vosk_model_free(model);
return 1;
}
ret = snd_pcm_set_params(pcm,
SND_PCM_FORMAT_S16_LE,
SND_PCM_ACCESS_RW_INTERLEAVED,
1, // 单声道
SAMPLE_RATE,
1, // 允许重采样
1000000); // 缓冲区时间(微秒)= 1秒
if (ret < 0) {
std::cerr << "无法设置音频参数: " << snd_strerror(ret) << std::endl;
snd_pcm_close(pcm);
vosk_recognizer_free(rec);
vosk_model_free(model);
return 1;
}
// 3. 启动翻译线程
std::thread translator(translator_thread);
// 4. 主循环:采集音频并识别
std::vector<int16_t> buffer(BUFFER_FRAMES);
std::cout << "开始录音(按 Ctrl+C 退出)..." << std::endl;
while (running) {
int nread = snd_pcm_readi(pcm, buffer.data(), BUFFER_FRAMES);
if (nread < 0) {
snd_pcm_recover(pcm, nread, 0);
continue;
}
// 送入 Vosk(注意长度单位是字节)
int final = vosk_recognizer_accept_waveform(rec, reinterpret_cast<char*>(buffer.data()), nread * sizeof(int16_t));
if (final) {
std::string english = extract_text(vosk_recognizer_result(rec));
if (!english.empty()) {
{
std::lock_guard<std::mutex> lock(queue_mutex);
message_queue.push(english);
}
queue_cv.notify_one();
}
}
// 部分结果可选择性输出,此处忽略以保持翻译流完整
}
// 5. 最后处理残留结果
std::string final_text = extract_text(vosk_recognizer_final_result(rec));
if (!final_text.empty()) {
std::lock_guard<std::mutex> lock(queue_mutex);
message_queue.push(final_text);
}
queue_cv.notify_all();
// 6. 等待翻译线程结束并清理资源
if (translator.joinable()) translator.join();
snd_pcm_close(pcm);
vosk_recognizer_free(rec);
vosk_model_free(model);
std::cout << "程序正常退出。" << std::endl;
return 0;
}
6. 模型准备与环境配置
6.1 安装依赖库
在 Ubuntu/Debian 系统上:
bash
sudo apt install libasound2-dev libvosk-dev libctranslate2-dev libsentencepiece-dev
如果官方源没有 CTranslate2 开发包,可以从 GitHub Release 下载预编译的库,或通过 pip 安装 Python 版后提取动态库,或自行编译。
6.2 下载 Vosk 英文模型
bash
wget https://alphacephei.com/vosk/models/vosk-model-en-us-0.22.zip
unzip vosk-model-en-us-0.22.zip
6.3 获取并转换 OPUS-MT 英中翻译模型
-
下载 OPUS-MT 模型文件(Hugging Face):
bashgit clone https://huggingface.co/Helsinki-NLP/opus-mt-en-zh -
安装 CTranslate2 的 Python 包用于转换:
bashpip install ctranslate2 sentencepiece -
转换模型:
pythonimport ctranslate2 ctranslate2.convert("opus-mt-en-zh", "opus-mt-en-zh_ct2")(将生成
opus-mt-en-zh_ct2目录,即代码中的CT2_MODEL_PATH) -
将
opus-mt-en-zh/source.spm和opus-mt-en-zh/target.spm拷贝到程序目录,对应SRC_SP_MODEL和TGT_SP_MODEL。
6.4 编译程序
bash
g++ -std=c++17 -O2 -pthread \
vosk_realtime_translate.cpp \
-o vosk_realtime_translate \
-lasound -lvosk -lctranslate2 -lsentencepiece
若 CTranslate2 或 SentencePiece 为自定义安装路径,需添加 -I 和 -L 选项。
7. 运行效果与调优建议
运行程序后,对着麦克风说英文,控制台将实时输出翻译后的中文。例如:
开始录音(按 Ctrl+C 退出)...
翻译模型加载完毕,开始实时翻译...
Hello, how are you?
你好,你好吗?
The weather is nice today.
今天天气真好。
调优建议:
- 降低延迟 :减小 ALSA 缓冲区时间(
snd_pcm_set_params最后的参数),例如改为500000(0.5秒),同时减小BUFFER_FRAMES。但过小可能增加丢帧风险。 - 提高识别准确率 :可以设置 Vosk 的
vosk_recognizer_set_max_alternatives和vosk_recognizer_set_words等参数。 - GPU 加速 :若机器有 NVIDIA GPU,可将
Device::CPU改为Device::CUDA,翻译延迟可大幅降低。 - 部分结果翻译 :如果想看到"边说边译"的效果,可以在识别出部分结果(
vosk_recognizer_partial_result)时也推送到队列,但翻译会变得碎片化,适合实时字幕等场景。