Skip to content

Commit 49fd7e6

Browse files
committed
Add a post about integrating voice to speech service to ai-chatbox
1 parent 2482246 commit 49fd7e6

File tree

2 files changed

+559
-0
lines changed

2 files changed

+559
-0
lines changed

_org/2025-08-01-ai-box-vot.org

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
#+OPTIONS: ^:nil
2+
#+BEGIN_EXPORT html
3+
---
4+
layout: default
5+
title: AI聊天盒子 - 集成语音转文字服务
6+
tags: [LLM, Rust, ESP32, ESP32S3]
7+
nav_order: {{ page.date }}
8+
sync_wexin: 1
9+
---
10+
#+END_EXPORT
11+
12+
* AI聊天盒子 - 集成语音转文字服务
13+
14+
** 前言
15+
16+
好久没有更新文章了,最近在做一个智能键盘的项目,以后有机会给大家介绍。前面我们已经实现了和AI聊天的功能,然而我们还需要将用户的语音转成文字发送给AI,本文介绍如何集成语音转文字服务。
17+
18+
本系列其他文章
19+
1. [[https://paul356.github.io/2025/03/15/wake-word-detect.html][AI聊天盒子 - 概念设计]]
20+
2. [[https://paul356.github.io/2025/04/09/ai-box-esp-sr.html][AI聊天盒子 - 使用esp-sr和Rust语言编写语音识别程序]]
21+
3. [[https://paul356.github.io/2025/04/25/ai-box-ask-llm.html][AI聊天盒子 - 请AI帮我编写和AI聊天的Rust代码]]
22+
23+
** 语音转文字服务
24+
25+
语音转文字服务有很多成熟的项目可以使用,我选择了Vosk语音识别工具包。要使用Vosk,可以通过pip安装Vosk模块,另外还需要从链接[[https://alphacephei.com/vosk/models/vosk-model-cn-0.22.zip][vosk-model-cn-0.22.zip]]下载模型文件,解压后待用。
26+
27+
#+begin_src txt
28+
pip install vosk flask flask-cors
29+
#+end_src
30+
31+
结合Flask模块我们就可以快速实现一个基于Vosk的REST服务,创建一个名为vosk_server.py的文件,填入下列代码。
32+
33+
#+begin_src python
34+
import tempfile
35+
36+
app = Flask(__name__)
37+
CORS(app) # 允许跨域请求(局域网内其他设备访问)
38+
39+
# 加载Vosk模型(替换为你的模型路径)
40+
model = Model("vosk-model-cn-0.22") # 中文模型
41+
42+
@app.route('/transcribe', methods=['POST'])
43+
def transcribe():
44+
if 'file' not in request.files:
45+
return jsonify({"error": "No audio file provided"}), 400
46+
47+
audio_file = request.files['file']
48+
_, temp_path = tempfile.mkstemp(suffix=".wav")
49+
audio_file.save(temp_path)
50+
51+
# 使用Vosk进行语音识别
52+
recognizer = KaldiRecognizer(model, 16000)
53+
with open(temp_path, 'rb') as f:
54+
audio_data = f.read()
55+
recognizer.AcceptWaveform(audio_data)
56+
57+
os.remove(temp_path) # 删除临时文件
58+
result = recognizer.FinalResult()
59+
return result
60+
#return jsonify({"text": result[0]})
61+
62+
if __name__ == '__main__':
63+
app.run(host='0.0.0.0', port=5000) # 允许局域网访问
64+
#+end_src
65+
66+
我们到目录vosk-model-cn-0.22的上一级目录下运行 ~python vosk_server.py~ ,我们就启动了一个简单的语音转文字服务。这个语音转文字服务,接受一个wav文件,然后将语音转换成文字返回给调用者。我们可以使用下面的命令来测试这个REST服务。
67+
68+
#+begin_src txt
69+
curl -X POST -F "[email protected]" http://127.0.0.1:5000/transcribe
70+
#+end_src
71+
72+
** 集成语音转文字服务
73+
74+
在[[https://paul356.github.io/2025/04/09/ai-box-esp-sr.html][AI聊天盒子 - 使用esp-sr和Rust语言编写语音识别程序]]中我们已经实现了一个简单的AI对话流程。这里我们先回顾一下这个流程。
75+
76+
[[/images/ai-chatbox-fetch-states.png]]
77+
78+
1. 用户需要先使用唤醒词“hi, 乐鑫”唤醒程序。
79+
2. 用户再使用命令词“我有个问题”进入录音状态。
80+
3. 记录用户的语音。
81+
4. 程序检测到2秒连续的静音,返回等待唤醒检测的状态。
82+
83+
我将第4步进行了扩展,检测到2秒连续的静音后,我们就会开始语音转换流程。然后将转换结果发送给LLM,再获取并在日志中打印LLM的回答。扩展后的流程如下。
84+
85+
1. 用户需要先使用唤醒词“hi, 乐鑫”唤醒程序。
86+
2. 用户再使用命令词“我有个问题”进入录音状态。
87+
3. 记录用户的语音。
88+
4. 程序检测到2秒连续的静音:
89+
1) 使用语音转文字服务将语音转换为文字。
90+
2) 得到文字形态而问题后,使用LLM API向DeepSeek提问。
91+
3) 获得回答打印到日志中。
92+
93+
为了实现增加的功能我创建了一个transcription_worker线程负责将语音转换成文字和向LLM提问,这样可以减少对fetch_proc的影响,保证AFE数据队列不会堵塞。
94+
95+
#+begin_src Rust
96+
// Worker function for the transcription thread
97+
fn transcription_worker(rx: Receiver<TranscriptionMessage>) -> anyhow::Result<()> {
98+
log::info!("Transcription worker thread started");
99+
100+
// Get token from environment variable at compile time
101+
let token = env!("LLM_AUTH_TOKEN", "LLM authentication token must be set at compile time");
102+
103+
// Create and configure the LLM helper
104+
let mut llm = match llm_intf::LlmHelper::new(token, "deepseek-chat") {
105+
helper => {
106+
log::info!("LLM helper created successfully");
107+
helper
108+
}
109+
};
110+
111+
// Configure with parameters suitable for embedded device
112+
llm.configure(
113+
Some(512), // Max tokens to generate in response
114+
Some(0.7), // Temperature - balanced between deterministic and creative
115+
Some(0.9) // Top-p - slightly more focused sampling
116+
);
117+
118+
// Send initial system message to set context
119+
llm.send_message(
120+
"接下来的请求来自一个语音转文字服务,请小心中间可能有一些字词被识别成同音的字词。".to_string(),
121+
ChatRole::System
122+
);
123+
124+
log::info!("LLM helper initialized with system prompt");
125+
126+
loop {
127+
match rx.recv() {
128+
Ok(TranscriptionMessage::TranscribeFile { path }) => {
129+
log::info!("Received request to transcribe file: {}", path);
130+
131+
match transcribe_audio(&path) {
132+
Ok(transcription) => {
133+
log::info!("Transcription completed: {}", transcription);
134+
135+
// Send the transcription to the LLM
136+
log::info!("Sending transcription to LLM...");
137+
let response = llm.send_message(transcription, ChatRole::User);
138+
139+
if response.starts_with("Error:") {
140+
log::error!("LLM API error: {}", response);
141+
} else {
142+
log::info!("LLM response: {}", response);
143+
144+
// Here you would send the response to a text-to-speech system
145+
// For now, we just log it
146+
}
147+
},
148+
Err(e) => {
149+
log::error!("Failed to transcribe audio: {}", e);
150+
}
151+
}
152+
},
153+
Ok(TranscriptionMessage::Shutdown) => {
154+
log::info!("Transcription worker received shutdown signal");
155+
break;
156+
},
157+
Err(e) => {
158+
log::error!("Error receiving message in transcription worker: {}", e);
159+
break;
160+
}
161+
}
162+
}
163+
164+
log::info!("Transcription worker thread terminated");
165+
Ok(())
166+
}
167+
#+end_src
168+
169+
并在inner_fetch_proc的Recording状态中通过管道给transcription_worker发送消息,触发语音转换流程。
170+
171+
#+begin_src Rust
172+
// Finalize WAV file
173+
if let Some(writer) = wav_writer.take() {
174+
log::info!("Finalizing WAV file after {} silent frames", silence_frames);
175+
writer.finalize()?;
176+
177+
// Flush the filesystem to ensure all data is written
178+
if let Err(e) = flush_filesystem("/vfat") {
179+
log::warn!("Failed to flush filesystem: {}", e);
180+
} else {
181+
log::info!("Filesystem flushed successfully");
182+
183+
// Send a message to the transcription thread to process the file
184+
let file_path = format!("/vfat/audio{}.wav", file_idx - 1);
185+
if let Err(e) = arg.transcription_tx.send(TranscriptionMessage::TranscribeFile { path: file_path }) {
186+
log::error!("Failed to send transcription message: {}", e);
187+
} else {
188+
log::info!("Sent audio file for asynchronous transcription");
189+
}
190+
}
191+
}
192+
#+end_src
193+
194+
具体的代码修改可以查看代码仓[[https://github.com/paul356/ai-chatbox][ai-chatbox]]。
195+
196+
** 效果测试
197+
198+
我还是使用XIAO ESP32S3 Sense作为测试硬件。为了避免手动刷写模型文件,我把srmodels存到了SD卡上,除了代码修改中包含的配置文件skconfig.defaults的修改和将esp_srmodel_init的参数改为“/vfat”之外,还要把srmodels中的四个子目录mn7_cn、nsnet2、vadnet1_medium、wn9_hilexin整体拷贝到SD卡的根目录下。
199+
200+
#+begin_src shell
201+
> ls ./target/xtensa-esp32s3-espidf/debug/build/esp-idf-sys-*/out/build/srmodels
202+
fst mn7_cn nsnet2 srmodels.bin vadnet1_medium wn9_hilexin
203+
#+end_src
204+
205+
但这个修改其实不是必须的,主要是为了刷写固件更加方便。下面运行 ~env WIFI_SSID=<ssid-name> WIFI_PASS=<wifi-password> LLM_AUTH_TOKEN=<llm-auth-token> cargo espflash flash -p /dev/ttyACM0 --flash-size 8mb~ 命令刷写好固件,再用 ~cargo espflash monitor~ 查看运行日志。下面是我测试时的运行日志,我问了DeepSeek一个问题“大模型是什么东西”。不过可能由于Vosk模型比较小,发音需要比较清楚才能准确识别。
206+
207+
#+begin_src txt
208+
I (61892) ai_chatbox: State transition: WakeWordDetecting -> CmdDetecting (Waiting for wake word → Detecting command): Wake word detected
209+
I (64512) ai_chatbox: Command detected: 1
210+
I (64512) ai_chatbox: State transition: CmdDetecting -> Recording (Detecting command → Recording audio): Command detected (ID: 1)
211+
I (64522) ai_chatbox: Creating WAV file: /vfat/audio1.wav
212+
I (70822) ai_chatbox: State transition: Recording -> WakeWordDetecting (Recording audio → Waiting for wake word): Detected 66 frames of silence
213+
I (70822) ai_chatbox: Finalizing WAV file after 66 silent frames
214+
I (70852) ai_chatbox: Filesystem at /vfat flushed successfully
215+
I (70852) ai_chatbox: Filesystem flushed successfully
216+
I (70852) ai_chatbox: Received request to transcribe file: /vfat/audio1.wav
217+
I (70862) ai_chatbox: Sent audio file for asynchronous transcription
218+
I (70862) ai_chatbox: Transcribing audio file: /vfat/audio1.wav
219+
I (71132) ai_chatbox: Read 205868 bytes from WAV file
220+
I (72612) ai_chatbox: Response status: 200
221+
I (72612) ai_chatbox: Transcription completed: {
222+
"text" : "大 模型 是 什么 东西"
223+
}
224+
I (72612) ai_chatbox: Sending transcription to LLM...
225+
I (72622) ai_chatbox::llm_intf: Sending request to DeepSeek API...
226+
I (72622) ai_chatbox::llm_intf: Initiating HTTP request to https://api.deepseek.com/chat/completions
227+
I (72772) esp-x509-crt-bundle: Certificate validated
228+
I (73462) ai_chatbox::llm_intf: HTTP request sent successfully.
229+
I (73462) ai_chatbox::llm_intf: HTTP response status: 200
230+
I (94142) ai_chatbox::llm_intf: Response received. Tokens used: 124 (prompt) + 390 (completion) = 514 (total)
231+
I (94142) ai_chatbox: LLM response: **大模型(Large Language Model, LLM)** 是一种基于海量数据训练的**人工智能模型**,能够理解和生成人类语言(甚至代码、多模态内容等)。它的核心特点是:
232+
233+
---
234+
235+
### 1. **本质是什么?**
236+
- **参数规模超大**:通常拥有百亿、千亿甚至万亿级参数(比如GPT-3有1750亿参数),通过深度学习(如Transformer架构)从文本数据中学习语言规律。
237+
- **通用性强**:不像传统AI专精单一任务(如翻译),大模型能处理问答、写作、编程、逻辑推理等多种任务。
238+
239+
---
240+
241+
### 2. **为什么叫“大”?**
242+
- **数据大**:训练数据可达TB级(全网文本、书籍、代码等)。
243+
- **算力大**:需要超算级GPU集群训练,成本极高(例如GPT-3训练费用超千万美元)。
244+
- **能力“大”**:涌现出小模型不具备的复杂能力(如上下文学习、少样本推理)。
245+
246+
---
247+
248+
### 3. **常见例子**
249+
- **ChatGPT**(OpenAI)、**Gemini**(Google)、**Claude**(Anthropic)等对话式AI。
250+
- **文心一言**(百度)、**通义千问**(阿里)等中文大模型。
251+
252+
---
253+
254+
### 4. **能干什么?**
255+
- 生成文章、代码、剧本
256+
- 解答复杂问题(需验证)
257+
- 翻译/总结文档
258+
- 作为智能助手(客服、教育等)
259+
260+
---
261+
262+
### 5. **局限性**
263+
- 可能产生“幻觉”(编造错误信息)
264+
- 依赖训练数据,存在偏见风险
265+
- 需大量算力,不够环保
266+
267+
如果需要更具体的解释(如技术原理、应用场景),可以告诉我!
268+
#+end_src
269+
270+
** 总结
271+
272+
本文主要考虑如何为ai-chatbox集成语音转文字服务,这样我们就可以用语音向LLM提问。如果进一步将获得的回答转成语音播放出来,就可以完整地和LLM语音聊天了,这将是下一篇文章我们要考虑的事情。
273+
274+
** 链接
275+
276+
1. vosk-model-cn-0.22.zip - https://alphacephei.com/vosk/models/vosk-model-cn-0.22.zip
277+
2. ai-chatbox - https://github.com/paul356/ai-chatbox

0 commit comments

Comments
 (0)