基于ESP32-S3与ReSpeaker的云端语音助手全栈开发实战
1. 项目概述:从硬件到云端的语音交互新玩法
最近在捣鼓一个挺有意思的项目,用ESP32-S3这块性能不错的开发板,搭配ReSpeaker麦克风阵列,做了一个能接入云端大模型的语音助手。这玩意儿和市面上那些智能音箱的内核思路有点像,但完全由自己掌控,从硬件选型、固件开发到云端服务对接,整个链路都能自己定制。ESP32-S3的双核处理器和充足的PSRAM,让它有能力在本地完成一些基础的语音唤醒和前端处理,然后把高质量的音频流推到云端,让更强大的AI模型来处理复杂的语义理解和对话生成。对于想深入理解智能语音交互全栈流程,或者想打造一个完全私有的、可高度定制的语音助手的朋友来说,这个组合是个非常理想的起点。
它解决的不仅仅是“让设备能听懂话”的问题,更是提供了一个从端侧感知到云端智能的完整实践案例。你可以学习到如何为嵌入式设备选择合适的音频采集硬件,如何设计低延迟、高可靠的音频数据传输协议,以及如何与云端AI服务进行安全、高效的交互。无论是用于智能家居的中控、个性化的办公助手,还是作为学习AIoT(人工智能物联网)的绝佳项目,这套方案都极具参考价值。接下来,我就把自己在搭建过程中梳理的设计思路、踩过的坑和总结的经验,详细地拆解一遍。
2. 核心硬件选型与平台搭建解析
2.1 为什么是ESP32-S3与ReSpeaker的组合?
选择ESP32-S3作为主控,是经过多方面权衡的。相较于经典的ESP32,ESP32-S3增加了第二个Xtensa® LX7核心,主频高达240MHz,并且标配了外部PSRAM支持(我用的型号是8MB)。这意味着它有更强的算力来处理音频预处理任务,比如回声消除、噪声抑制,或者运行一个轻量级的本地唤醒词识别引擎。同时,其Wi-Fi和蓝牙5.0的连接能力非常稳定,为音频流实时上传到云端提供了坚实的基础。另一个关键是它的IO接口丰富,可以轻松对接I2S数字麦克风。
而ReSpeaker麦克风阵列板,则是为了提升远场语音交互的体验。我使用的是ReSpeaker 2-Mics Pi HAT(兼容ESP32-S3的版本)或核心类似的阵列模块。它集成了2个或更多数字麦克风,通过板载的音频编解码芯片(如AC108)形成麦克风阵列,能够实现声源定位和波束成形。简单来说,就是能让设备“聚焦”在说话人的方向,有效抑制其他方向的环境噪音,在几米外也能清晰地拾取你的指令。这对于实际家居环境下的使用体验是质的提升。ESP32-S3通过I2S和I2C总线与ReSpeaker板通信,一个负责传输音频数据,一个负责配置编解码芯片的参数。
2.2 开发环境与基础固件准备
第一步是搭建开发环境。我强烈推荐使用Visual Studio Code配合PlatformIO插件,而不是传统的Arduino IDE。PlatformIO对库依赖管理和项目构建的支持更专业,特别适合这种需要集成多个复杂库(如音频处理、HTTP/WebSocket客户端)的项目。
- 创建PlatformIO项目 :在VSCode中新建项目,选择板卡为“Espressif ESP32-S3-DevKitC-1-N8R8”(具体根据你的开发板型号选择,关键是要支持PSRAM)。
-
关键库依赖
:在项目的
platformio.ini文件中,需要声明以下核心库。这些库不是凭空想象的,而是经过社区验证、能协同工作的组合。[env:esp32-s3-devkitc-1] platform = espressif32 board = esp32-s3-devkitc-1 framework = arduino monitor_speed = 115200 ; 启用PSRAM board_build.arduino.memory_type = qio_opi ; 库依赖 lib_deps = esphome/ESP32-audioI2S @ ^2.0.7 arduino-libraries/ArduinoHttpClient @ ^0.4.0 links2004/WebSockets @ ^2.3.6 olikraus/ESP32-audioTools @ ^1.0.0-
ESP32-audioI2S:用于驱动I2S接口,从ReSpeaker板卡采集和播放音频。 -
ArduinoHttpClient和WebSockets:用于与云端服务建立HTTP或WebSocket连接,后者更适合流式音频传输的低延迟需求。 -
ESP32-audioTools:提供了一系列音频编解码、流处理的工具,非常实用。
-
-
硬件连接与测试
:将ReSpeaker板与ESP32-S3正确连接。通常需要连接:
- I2S :BCLK(位时钟)、WS(字选择)、DOUT(数据输出,从ReSpeaker到ESP32)、DIN(数据输入,用于播放,可选)。
- I2C :SDA、SCL,用于配置AC108等芯片。
- 电源 :3.3V和GND。 连接好后,可以先上传一个简单的I2S麦克风采集示例代码,通过串口监视器查看是否有音频数据输出,确保硬件链路通畅。
注意 :不同型号的ReSpeaker板卡引脚定义可能不同,务必查阅其对应的数据手册或Wiki页面。供电要稳定,不稳定的电源会在音频数据中引入可闻的噪声。
3. 音频采集、预处理与流式上传实现
3.1 高质量音频采集管道搭建
音频采集是整个系统的源头,质量至关重要。我们需要配置ESP32-S3的I2S控制器以正确的参数从ReSpeaker读取数据。
#include <Audio.h>
#include <driver/i2s.h>
// I2S引脚定义(根据你的接线调整)
#define I2S_MIC_BCLK 15
#define I2S_MIC_WS 16
#define I2S_MIC_DOUT 17
// I2S配置
i2s_config_t i2s_mic_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX), // 主模式,接收
.sample_rate = 16000, // 采样率16kHz,对于语音识别足够,且节省带宽
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, // 接收32位数据
.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, // 单声道,通常用右通道
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8, // DMA缓冲区数量
.dma_buf_len = 512, // 每个缓冲区长度
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t i2s_mic_pins = {
.bck_io_num = I2S_MIC_BCLK,
.ws_io_num = I2S_MIC_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_MIC_DOUT
};
void setup_audio() {
esp_err_t err = i2s_driver_install(I2S_NUM_0, &i2s_mic_config, 0, NULL);
if (err != ESP_OK) {
Serial.printf("I2S驱动安装失败: %d\n", err);
return;
}
err = i2s_set_pin(I2S_NUM_0, &i2s_mic_pins);
if (err != ESP_OK) {
Serial.printf("I2S引脚设置失败: %d\n", err);
return;
}
// 如果需要,通过I2C配置ReSpeaker板载的AC108芯片增益等参数
// configure_ac108();
}
这里选择16kHz采样率、单声道,是权衡了音质、处理负担和网络带宽后的常见选择。32位位深是因为I2S接口通常以32位帧传输数据,即使ADC精度可能只有24位或更低。
3.2 实时音频预处理与VAD(语音活动检测)
原始音频数据不能直接上传。首先,我们需要进行增益归一化和可能的滤波,以消除一些固定频率的噪声。更关键的一步是实施 语音活动检测(VAD) 。VAD能判断当前时间段内是否有语音,只有在检测到人声时才启动上传,从而极大节省云端处理资源和网络流量。
我采用了一个在MCU上资源消耗较低的算法:基于短时能量和过零率的双门限VAD。
bool voice_activity_detect(int16_t* audio_buffer, size_t len) {
long energy = 0;
int zcr = 0;
int16_t prev_sample = audio_buffer[0];
for (size_t i = 0; i < len; i++) {
int16_t sample = audio_buffer[i];
energy += (long)sample * sample;
if ((sample > 0 && prev_sample <= 0) || (sample < 0 && prev_sample >= 0)) {
zcr++;
}
prev_sample = sample;
}
float avg_energy = (float)energy / len;
float avg_zcr = (float)zcr / len;
// 这些阈值需要在实际环境中通过实验校准
const float energy_threshold = 1000.0; // 能量阈值
const float zcr_threshold_low = 5.0; // 过零率低阈值(滤除高频噪声)
const float zcr_threshold_high = 50.0; // 过零率高阈值(滤除清音等)
return (avg_energy > energy_threshold) &&
(avg_zcr > zcr_threshold_low) &&
(avg_zcr < zcr_threshold_high);
}
在循环中,我们从I2S DMA缓冲区读取数据,先进行VAD判断。如果检测到语音,则开启一个“录音会话”,持续采集音频直到VAD检测到一段静音(例如持续1秒无声)。这个会话内的所有音频数据,才会被打包上传。
3.3 通过WebSocket流式上传音频数据
为了达到低延迟的交互效果,我们采用WebSocket协议与云端服务建立全双工的长连接。音频数据在采集的同时,就被分块发送出去。云端模型可以边接收边处理,实现“实时”的流式识别。
-
建立WebSocket连接
:使用WebSocket客户端库连接到云端服务的WS端点(例如:
wss://your-ai-service.com/v1/asr/stream)。连接时需要携带认证信息,通常是API Key,可以放在HTTP Header中。// 伪代码示例 webSocket.beginSSL("your-ai-service.com", 443, "/v1/asr/stream"); webSocket.setExtraHeaders("Authorization: Bearer your_api_key_here\r\n"); webSocket.onEvent(webSocketEventHandler); // 设置事件回调 webSocket.begin(); -
音频编码与发送
:为了减少传输数据量,通常会对PCM音频进行压缩编码。Opus编码器在低码率下对语音有很好的保真度,但它在MCU上编码计算量较大。一个更轻量的方案是使用
G.711 (A-law或μ-law)
,它复杂度极低,只是将16位PCM压缩为8位,压缩比为2:1,对于16kHz采样率,码率仅为128kbps,对于局域网或良好Wi-Fi环境是可接受的。我们可以使用
audio_tools库中的G711Encoder。#include "AudioTools.h" G711Encoder g711_encoder; void setup() { auto g711_config = g711_encoder.defaultConfig(); g711_config.channels = 1; g711_config.sample_rate = 16000; g711_encoder.begin(g711_config); } // 在音频采集循环中 size_t bytes_read = i2s_read(I2S_NUM_0, pcm_buffer, sizeof(pcm_buffer), &bytes_read, portMAX_DELAY); size_t encoded_len = g711_encoder.encode(g711_buffer, pcm_buffer, bytes_read); webSocket.sendBIN(g711_buffer, encoded_len); // 发送二进制数据 - 协议封装 :有些云端ASR(自动语音识别)服务要求音频数据封装在特定的协议帧里,比如前端发送一个JSON配置帧,后面跟随二进制音频帧。你需要根据云端API文档来构建这些数据包。
实操心得 :WebSocket连接在恶劣网络环境下可能会断开。务必实现稳健的重连逻辑和心跳机制(例如每30秒发送一个Ping帧)。在
webSocketEventHandler回调函数中,监听断开事件,并尝试指数退避重连。同时,发送音频数据的循环中要加入流量控制,避免发送速度远超采集速度导致内存堆积,可以使用一个队列(Ring Buffer)来缓冲。
4. 云端AI服务对接与对话逻辑设计
4.1 云端ASR与LLM服务选型与调用
音频流上传到云端后,首先需要被转写成文字(ASR),然后将文字送入大语言模型(LLM)生成回复,最后可能需要将回复文字再转成语音(TTS)下发给设备。你可以选择一体化的语音助手API(如一些云厂商提供的服务),也可以自己组合不同的API。
我倾向于后者,因为它更灵活,也便于理解各环节。例如:
- ASR服务 :可以选择专门提供流式语音识别的服务,它们通常延迟更低、准确率更高。
- LLM服务 :调用如GPT、文心一言、通义千问等大模型的API。关键是设计好 系统提示词(System Prompt) ,将助手的人格、能力范围、回复格式定义清楚。
- TTS服务 :将LLM返回的文本合成语音,可以选择声音自然、支持情感合成的服务。
在ESP32端,我们主要与一个 自建的中继服务器 或 集成了上述流程的云端逻辑 (如使用Serverless函数)通信。这样,ESP32只需要和一个端点对话,复杂性被封装在后端。
WebSocket连接建立后,通信协议可以这样设计:
- ESP32发送一个开始消息,包含音频格式、采样率等信息。
- ESP32持续发送音频二进制数据帧。
-
云端ASR服务实时返回中间识别结果(如
{"type": "partial", "text": "今天天"})和最终结果({"type": "final", "text": "今天天气怎么样?"})。 - 当云端检测到一句话结束(端点检测),便将最终文本传递给LLM。
- LLM生成回复文本,再触发TTS服务生成音频流。
- 云端通过同一个WebSocket连接,将TTS音频流(或至少是回复文本)发回ESP32。
4.2 ESP32端的响应处理与播放
当ESP32通过WebSocket接收到数据后,需要在事件回调函数中解析:
void webSocketEventHandler(WStype_t type, uint8_t * payload, size_t length) {
switch(type) {
case WStype_TEXT: // 收到文本消息,可能是识别结果或LLM回复
DynamicJsonDocument doc(1024);
deserializeJson(doc, payload);
const char* msgType = doc["type"];
if(strcmp(msgType, "final_asr") == 0) {
// 显示识别出的文字到屏幕(如果有)
String query = doc["text"];
Serial.print("用户说: ");
Serial.println(query);
} else if(strcmp(msgType, "llm_response") == 0) {
String answer = doc["text"];
Serial.print("助手回复: ");
Serial.println(answer);
// 如果需要本地TTS,可以在这里处理文本
// 但更常见的是接收云端发来的TTS音频流
}
break;
case WStype_BIN: // 收到二进制消息,很可能是TTS音频流
// 假设是G.711编码的音频
decode_and_play_audio(payload, length); // 解码并通过I2S播放
break;
case WStype_DISCONNECTED:
// 触发重连逻辑
break;
}
}
对于播放,我们需要初始化另一个I2S端口(或复用)连接到扬声器。收到二进制音频数据后,如果是压缩格式(如G.711、Opus),需要先解码成PCM,再写入I2S的发送缓冲区。
注意事项 :音频播放的实时性要求很高。如果网络抖动导致TTS音频数据包到达不均匀,直接播放会有卡顿。一个实用的技巧是建立一个小的 播放缓冲队列 (例如缓存200ms的音频数据)。播放线程从队列中稳定地取出数据播放,而网络接收线程将数据填入队列。当队列数据低于某个阈值时,可以稍微拉长播放间隔(类似“慢放”)以避免断流,但这可能会造成音调变化。更优的方案是使用能处理网络抖动的音频播放库。
5. 低功耗与唤醒词设计优化
5.1 本地轻量级唤醒词引擎集成
让设备持续监听云端ASR是不现实且耗电的。因此,需要集成一个本地唤醒词检测引擎。常见的开源方案是 ESP-SR(Espressif Speech Recognition) ,乐鑫官方提供了针对ESP32系列的优化版唤醒词模型。
- 模型选择与集成 :从乐鑫的GitHub仓库获取ESP-SR组件。它提供了一些预训练的唤醒词模型(如“Hi,乐鑫”、“小爱同学”等),也支持自定义唤醒词训练(需要准备数据集并使用其工具链)。将组件添加到你的PlatformIO项目中。
-
工作流程
:主循环中,持续从I2S采集短帧音频(例如16ms一帧),送入唤醒词引擎进行推断。
#include "esp_sr.h" esp_sr_wakenet_t *wakenet = esp_sr_wakenet_init("wn9_hilexin"); // 加载模型 int16_t audio_buffer[FRAME_LENGTH]; // FRAME_LENGTH根据模型要求定义 void loop() { i2s_read(...); // 采集一帧音频到audio_buffer int keyword_id = esp_sr_wakenet_detect(wakenet, audio_buffer); if(keyword_id >= 0) { Serial.println("唤醒词检测到!"); // 触发后续动作:点亮LED,开始上传音频到云端ASR start_voice_assistant_session(); } // 如果没有唤醒,系统可以进入浅睡眠,由定时器或I2S DMA中断来触发下一次采集,以节省功耗。 } - 功耗管理 :在非唤醒状态,ESP32-S3可以进入 Light-sleep 模式,此时CPU暂停,但RTC和部分外设(如I2S的DMA)可以通过定时器唤醒。配置I2S在DMA缓冲区满时产生中断,唤醒系统处理一帧数据并进行唤醒词判断,然后立即再次休眠。这样可以极大降低待机功耗。
5.2 双麦阵列的声源定位(DOA)应用
ReSpeaker的双麦克风阵列除了降噪,还可以实现简单的声源定位。通过计算声音到达两个麦克风的时间差(TDOA),可以估算出声音的大致方向角。
// 简化示例:计算互相关函数寻找时延
float compute_delay(int16_t* mic1, int16_t* mic2, size_t len) {
long max_corr = -1e9;
int best_delay = 0;
int search_range = 10; // 假设最大时延对应10个采样点(根据麦克风间距和声速估算)
for(int d = -search_range; d <= search_range; d++) {
long corr = 0;
for(size_t i = 0; i < len - abs(d); i++) {
int idx1 = i + (d > 0 ? d : 0);
int idx2 = i + (d > 0 ? 0 : -d);
corr += (long)mic1[idx1] * mic2[idx2];
}
if(corr > max_corr) {
max_corr = corr;
best_delay = d;
}
}
return (float)best_delay / SAMPLE_RATE; // 返回以秒为单位的时延
}
得到时延后,根据麦克风间距和声速,可以换算成角度。这个角度信息可以很有用,例如,你可以让设备上的LED灯环指向说话人的方向,或者在多房间场景下,判断指令来自哪个区域。
踩坑记录 :声源定位在室内多径反射环境下很容易不准。不要指望它能给出精确的角度,但用于判断“左/右”或“前/后”的大致区域是可行的。为了提高鲁棒性,可以连续计算多帧的时延,然后取中位数或均值。此外,只有在VAD检测到有效语音时才进行DOA计算,可以减少噪声干扰。
6. 系统集成、调试与性能优化实战
6.1 整体状态机与代码架构设计
一个稳健的语音助手需要一个清晰的状态机来管理其行为。我设计的状态机通常包含以下几个状态:
- SLEEP :低功耗休眠状态,仅运行本地唤醒词检测。
- LISTENING :唤醒后,持续采集音频,进行VAD检测。如果VAD超时未检测到语音,则退回SLEEP。
- STREAMING :VAD检测到语音后,开启WebSocket连接,并开始流式上传音频数据。
- PROCESSING :已上传完一句话(VAD检测到静音),等待云端返回LLM回复和TTS音频。此状态可以播放一个“思考中”的提示音。
- SPEAKING :接收并播放TTS音频流。
- ERROR :网络断开、服务异常等,尝试恢复或复位。
在
loop()
函数中,根据当前状态执行相应的操作。使用非阻塞的编程模式,避免使用
delay()
,确保系统响应及时。
6.2 网络稳定性与断线重连策略
Wi-Fi连接和WebSocket长连接的稳定性是项目成败的关键。以下策略至关重要:
-
Wi-Fi智能连接
:在初始化时,不仅连接Wi-Fi,还要监听Wi-Fi事件(
WiFi.onEvent)。当遇到断开事件(SYSTEM_EVENT_STA_DISCONNECTED)时,不要立即重连,等待几秒并尝试重新扫描网络、选择最强信号。 -
WebSocket心跳与重连
:WebSocket客户端应设置自动Ping/Pong间隔(例如30秒)。在事件回调中,如果收到
WStype_DISCONNECTED,启动一个重连计时器。重连策略可以采用“指数退避”,比如第一次等待1秒,第二次2秒,第三次4秒……直到最大值,然后保持间隔重试。 -
数据发送的容错
:在
STREAMING状态发送音频数据时,每次调用webSocket.sendBIN()后检查返回值或连接状态。如果发送失败,应将数据暂存到备份缓冲区,并在连接恢复后重发,或者根据场景决定丢弃(对于实时语音,丢弃旧数据可能比延迟更可取)。
6.3 内存与性能监控优化
ESP32-S3虽然有PSRAM,但堆内存仍然有限。必须密切关注内存使用。
-
使用堆诊断工具
:
Serial.printf("Free Heap: %d, Min Free Heap: %d\n", ESP.getFreeHeap(), ESP.getMinFreeHeap()); -
避免动态内存碎片
:在循环中尽量避免频繁的
String拼接和malloc/free。对于固定的文本使用const char*,对于音频数据缓冲区使用全局或静态分配的数组。 -
任务优先级与核心绑定
:如果使用FreeRTOS(PlatformIO的Arduino框架默认启用),合理规划任务。例如:
- 高优先级任务:I2S音频采集中断服务、唤醒词检测。
- 中优先级任务:网络发送、接收处理。
- 低优先级任务:日志打印、状态指示灯更新。 可以将网络相关任务绑定到另一个核心(ESP32-S3是双核),实现真正的并行处理。
6.4 常见问题排查速查表
在实际部署中,你几乎一定会遇到下面这些问题。这里是我的排查清单:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 唤醒词误触发率高 | 环境噪音类似唤醒词发音;麦克风增益过高。 |
1. 调整唤醒词模型的检测阈值(如果模型支持)。
2. 在音频送入唤醒引擎前,增加软件增益控制或简单的噪声门限。 3. 尝试使用更复杂的唤醒词,如四音节词组。 |
| 云端识别文字不准 | 音频质量差;网络丢包;采样率/编码格式不匹配。 |
1. 用
i2s_read
原始数据存成WAV文件在电脑上播放,确认采集是否清晰。
2. 检查VAD是否过早截断了语音尾部。 3. 确认发送给云端的音频格式参数(采样率、位深、编码)与API要求完全一致。 4. 在Wi-Fi信号强的环境下测试。 |
| TTS播放卡顿或断断续续 | 网络抖动导致数据到达不均;播放缓冲区太小;I2S播放任务优先级低。 |
1. 增加播放缓冲队列大小(例如从100ms增加到300ms)。
2. 提高播放任务(或回调函数)的FreeRTOS优先级。 3. 检查是否在播放过程中有耗时操作(如大量串口打印)阻塞了任务。 |
| WebSocket频繁断开 | 网络不稳定;服务器端连接超时设置过短;未发送心跳包。 |
1. 实现并确保心跳(Ping/Pong)机制正常工作。
2. 在服务器端(如果可控)适当增加连接超时时间。 3. 在ESP32端,优化Wi-Fi重连逻辑,确保网络层稳定。 |
| 整体响应延迟大 | 各环节累积延迟:VAD静音检测、网络往返、云端处理、TTS生成。 |
1. 优化VAD参数,减少语句结束后的静音等待时间。
2. 考虑使用更快的云端服务区域或优化后端处理链路。 3. 对于简单指令,可以探索在端侧直接识别并执行(如“开灯”),绕过云端LLM。 |
7. 项目扩展与进阶玩法探讨
完成基础版本后,这个项目还有巨大的扩展空间:
- 离线命令词识别 :除了唤醒词,可以使用ESP-SR的 多命令词识别 模型,在本地识别一些高频、固定的指令,如“打开灯光”、“提高音量”。这可以实现零延迟的本地控制,即使断网也能工作。
- 上下文与多轮对话 :在云端LLM调用时,将历史对话的上下文也发送过去。这需要在ESP32或后端服务器中维护一个简单的对话历史缓存,使助手能记住之前的交流。
- 与智能家居平台集成 :让语音助手成为智能家居的语音入口。ESP32可以通过MQTT协议,将识别出的指令(如“打开客厅空调”)发布到Home Assistant、云云对接等平台,由平台去执行具体的设备控制。
- 自定义唤醒词与TTS声音 :利用乐鑫提供的工具,训练属于你自己的唤醒词(比如你的名字)。也可以探索将TTS声音替换成你喜欢的音色,甚至克隆自己的声音,这需要对接支持自定义声线的TTS API。
- 低功耗深度优化 :如果你希望设备用电池供电,需要深入优化功耗。测量各状态下的电流,使用深度睡眠(Deep Sleep),仅通过硬件GPIO中断(比如用一个按钮)或定时器来定期唤醒进行唤醒词检测。
这个项目就像一把钥匙,打开了嵌入式AI语音交互的大门。从硬件接线、驱动调试,到算法集成、网络通信,再到云端服务联调,每一步都需要耐心和细致的排查。最大的收获不是做出了一个能对话的盒子,而是打通了这条从物理世界的声音到云端智能再返回物理世界反馈的完整链路。当你对着自己组装的设备说话,并得到精准的回应时,那种成就感是无可替代的。建议你在实现基本功能后,不要停下,选择一个方向深入下去,比如优化降噪算法、设计更自然的对话交互,或是将其集成到一个具体的产品原型中,乐趣和挑战都在那里等着你。
更多推荐
所有评论(0)