ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32圆屏语音助手:端云分离架构与完整实现解析

ESP32圆屏语音助手:端云分离架构与完整实现解析 糖球系列写到第三篇今天聊一个很多朋友在评论区反复问过的点这块带圆形屏幕的 ESP32 设备到底跑的是什么模型我直接给结论——它压根不跑模型。一块 2.1 英寸的圆屏加上一颗 ESP32-S3本身就不是用来跑大语言模型的料。它真正干的事情是在后台当一个语音客户端把用户说的话录下来、传给云端后台再把后台返回的文字或语音放出来同时用圆屏显示当前交互状态。这篇文章就把整个架构、代码实现和踩坑过程完整拆开讲一遍适合正在用 ESP32 做语音交互设备、或者想把手头圆屏做成语音助手的朋友参考。1. 项目架构与设计思路为什么圆屏不跑模型只做后台语音客户端1.1 这块圆屏的算力天花板在哪先说硬件底子。ESP32-S3 是双核 240MHz 的 Xtensa 处理器内置大概 512KB SRAM虽然有 PSRAM 可以扩展但主流配置也就是 8MB 到 16MB。Flash 一般是 8MB。这个算力跑跑 LVGL 界面、音频采集、网络协议栈都够但拿去跑模型就非常吃力了。拿大语言模型举个例子。一个 7B 参数的量化模型光权重就要占 3GB 到 4GB 的存储推理时内存占用至少还要翻倍峰值算力需求在几十 TOPS 级别。ESP32-S3 的 ethos 之类硬件加速器根本没有光靠双核 CPU 去算一次推理等几分钟是常事。那语音识别和语音合成呢类似。本地跑一个小的唤醒词模型倒是可以比如 ESP-SR 里的 WakeNet几十 KB 的模型能认“你好糖球”这种固定唤醒词。但真正的语音识别ASR、语义理解LLM、语音合成TTS放在端侧都不现实。ASR 模型的参数量动辄几百 MBTTS 模型更不用说合成一句“今天天气怎么样”可能要卡好几秒。所以当时我给“糖球”做架构规划时很早就定了一个原则端侧只做三件事——采集声音、显示状态、播放音频云端后台负责所有“烧脑”的活。这个决策不是偷懒而是硬件算力边界下的最优解。1.2 客户端与服务端的职责划分这块 240x240 的圆屏本质上就是一个“话筒 屏幕 扬声器”的综合终端和手机上那些语音助手 App 是一个道理。真正常识、思考、组织语言的都是后台的服务器手机 App 只是把语音传上去再把结果放出来。我当时画了一个非常简单的职责表格现在直接贴出来环节由谁负责说明唤醒词识别设备端可选用轻量模型离线识别“你好糖球”语音采集设备端I2S 读取 MEMS 麦克风数据转成 PCM音频传输设备端 - 后台WebSocket 上行可带 Opus 压缩语音识别 ASR云端后台把 PCM 转成文字调用 ASR 服务语义理解 / 对话云端后台调用大模型接口生成回复文本语音合成 TTS云端后台把回复文本转成音频流回传设备音频播放设备端I2S 输出到功放和扬声器状态显示设备端LVGL 控制圆屏显示聆听/思考/回复状态会话管理云端后台维护多轮对话上下文客户端/服务端分离最直接的好处是模型升级不用动设备固件。今天后台换一个更强的模型圆屏所有用户立刻生效。要是把模型塞进设备里每次换模型都要让用户重新刷固件体验太糟糕了。另外把复杂计算放到后台还能让设备保持低功耗。ESP32-S3 只做录音和播放平均功耗在 100mA 以内电池供电也能撑好几小时。如果端侧跑大模型那电池基本是摆设。2. 硬件选型与整体方案落地2.1 主控、屏幕和麦克风的搭配“糖球”的核心硬件配置如下主控ESP32-S3-WROOM-1 模组8MB Flash 8MB Octal PSRAM。屏幕GC9A01 圆形 LCD240x240 分辨率SPI 接口。麦克风INMP441I2S 接口的 MEMS 麦克风单只。功放MAX98357AI2S 数字功放接 8Ω/1W 小喇叭。电源3.7V 锂电池 充放电一体板或用 USB 供电调试。选 ESP32-S3 而不是普通 ESP32主要看重两点一是双核性能更强可以单独分配一个核给音频任务另一个核跑 UI 和网络二是 PSRAM 支持更大LVGL 的缓冲区、音频缓冲、TTS 播放缓冲都能怼进 PSRAM不会挤占内部 RAM。PSRAM 这一点非常关键。8MB 的 Octal PSRAM 跑 LVGL 的圆屏界面绰绰有余。如果不用 PSRAMESP32-S3 那 512KB 内部 SRAM 很快就会见底尤其是同时开 Wi-Fi、WebSocket、I2S 和 LVGL 时内存会不够到系统崩溃。麦克风这块INMP441 是单只 I2S 麦克风接法很简单SCK 接 GPIO、WS 接 GPIO、SD 接 GPIO。它的优点是超低噪声-24dBFS 的灵敏度适合近距离语音交互。如果后续想要远场拾音可以上 ES7210 四麦克风阵列但成本和内存开销都会明显增加第一版先单麦打通流程。连线表我整理了一下给还没画原理图的朋友做个参考INMP441 引脚接 ESP32-S3SCKGPIO 4WSGPIO 5SDGPIO 6L/RGND选择左声道VDD3.3VGNDGNDMAX98357A 引脚接 ESP32-S3BCLKGPIO 15LRCGPIO 16DINGPIO 17VIN3.3V 或 5VGNDGND这里有个常见坑MAX98357A 需要给比较干净的电源如果直接和麦克风共用一个 3.3V LDO播放时扬声器的大电流驱动会让电压跌落导致麦克风采集到明显底噪。我的做法是功放单独用 5V 供电或者至少加一个 100uF 电容做退耦。2.2 关于本地模型的边界什么可以上端侧什么不能再说一次这个项目里模型确实没有一个跑在 ESP32 上但端侧也不是完全没有“智能”。我把它拆成三个层次第一层唤醒词识别。可以用 ESP-SR 的 WakeNet模型只有几十 KB识别固定唤醒词非常准功耗低。这一层在设备端做好处是用户说唤醒词时不需要把每一段音频都传到后台既省流量又保护隐私。第二层固定命令词识别。ESP-SR 的 MultiNet 可以离线识别“打开灯”“音量加”“下一首”这类固定短句不需要上云。如果你的应用场景就是控制智能家居那完全可以把这一层留在端侧。第三层开放对话。这就是必须上后台的活了。用户说“帮我写首诗”“说说今天有什么新闻”端侧那点算力和模型能力完全不够。这时候圆屏的角色就是一个语音客户端负责把用户的声音送到后台再把后台生成的回复拿回来播放。其实对于糖球这种桌面语音助手“离线的唤醒词 云端的开放对话”是最务实的组合。唤醒词在端侧响应速度极快对话在云端智商在线。两者搭配起来用户体感上会觉得设备非常聪明。3. 设备端代码架构与关键实现3.1 音频采集链路I2S 配置与拾音优化我用 ESP-IDF 开发设备端。音频采集用 I2S 接口16kHz 采样率32bit 数据位宽INMP441 是 24bit 有效数据存进 32bit 容器里。代码大概长这样#include driver/i2s.h #define I2S_MIC_SCK 4 #define I2S_MIC_WS 5 #define I2S_MIC_SD 6 void mic_init(void) { i2s_config_t i2s_config { .mode I2S_MODE_MASTER | I2S_MODE_RX, .sample_rate 16000, .bits_per_sample I2S_BITS_PER_SAMPLE_32BIT, .channel_format I2S_CHANNEL_FMT_RIGHT_LEFT, .communication_format I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags ESP_INTR_FLAG_LEVEL1, .dma_buf_count 8, .dma_buf_len 1024, .use_apll false, .tx_desc_auto_clear false, }; i2s_pin_config_t pin_config { .bck_io_num I2S_MIC_SCK, .ws_io_num I2S_MIC_WS, .data_out_num I2S_PIN_NO_CHANGE, .data_in_num I2S_MIC_SD, }; i2s_driver_install(I2S_NUM_0, i2s_config, 0, NULL); i2s_set_pin(I2S_NUM_0, pin_config); }几个参数我得解释一下避免大家照抄完不知道怎么调采样率设 16kHz是因为 ASR 服务普遍接受 16kHz 单声道 PCM太高浪费带宽太低影响识别率。bits_per_sample 用 32bit这是 INMP441 的物理格式读回来每个 sample 占 4 字节。真正有效数据是低 24bit处理时右移 8bit 转成 16bit PCM。dma_buf_count 和 dma_buf_len 决定 DMA 缓冲大小。8 个 1024 点的缓冲也就是 8 x 4KB 32KB这个大小可以让录音任务和网络发送任务并行工作不丢数据。如果缓冲太少Wi-Fi 阻塞时音频会丢包后台听起来就是一卡一卡的。采集完数据后我开了一个 FreeRTOS 任务循环从 I2S 读数据然后打包成 320 字节10ms16kHz/16bit的 PCM 块通过 WebSocket 发送到后台。这样每 10ms 发一次服务端收到的就是连续的语音流。发送伪代码大致是void audio_send_task(void *arg) { int16_t pcm_buf[160]; // 10ms 16kHz while (recording_active) { size_t bytes_read 0; i2s_read(I2S_NUM_0, pcm_buf, sizeof(pcm_buf), bytes_read, portMAX_DELAY); websocket_send_binary(pcm_buf, bytes_read); } }音频上行这块最容易踩的坑是说话声音稍微大一点后台收到的是削顶失真。原因是 INMP441 的输出增益偏高远场还行近场猛地喊一句就会爆。解决办法有两个一是端侧做一下自动增益控制AGC二是后台做音频归一化。我后来选择了后台做动态压缩因为端侧 MCU 算力太宝贵能省则省。3.2 与后台的通信协议设计设备端和后台之间我选了 WebSocket而不是 HTTP 轮询。原因很简单WebSocket 是全双工音频上行和 TTS 下行可以走同一条连接实时性有保障。HTTP 轮询在语音交互场景下延迟太高用户说完话要等好几秒才有响应体验很难受。整个通信协议我分成了信令和音频两类消息。信令用 JSON 文本音频直接用二进制帧。设备启动并连接上后台后先发一个 hello 包{ type: hello, device_id: sugar-ball-03, device_name: 糖球三号, capabilities: [audio_in, audio_out, display] }后台收到 hello 后回一个 hello_ack里面带上当前后台版本和模型配置信息。设备端把这个信息显示在圆屏上。接下来进入语音交互。流程是这样的用户说唤醒词或按下屏幕上的按钮设备开始录音。设备发一个 start_stream 信令并附带音频参数{ type: start_stream, sample_rate: 16000, bits: 16, channels: 1, voice_timeout: 3000 }voice_timeout 表示后台检测到 3 秒静音就自动结束语音流。设备持续发送二进制音频帧每帧 10ms PCM 数据。用户说完设备发 end_stream 信令表示本次语音输入结束。后台经过 ASR LLM 处理后先下发一个 state 信令{ type: state, state: thinking, message: 让我想想 }后台生成回复文本后下发 reply 信令{ type: reply, reply_id: r_12345, text: 今天广州晴25到32度。, state: speaking }设备收到 reply 后立刻在圆屏上显示文本字幕。后台音频合成完成后逐帧下发二进制 TTS 音频。每帧前面加一个 8 字节的头部前 4 字节是帧序号后 4 字节是音频长度。设备按序写入 I2S 输出播放。这套协议把“文本”和“语音”拆开来发好处是设备可以边打字边播放屏幕上的字幕比声音还先出现用户体感上响应更快。坏处是协议处理复杂度高一点但以 ESP32-S3 的能力完全扛得住。音频压缩方面我的建议是如果设备在局域网内部使用PCM 裸传就行省掉编解码的算力开销如果跨公网使用一定要上 Opus 压缩否则一小时的语音会话流量会非常可观。Opus 在 16kbps 下语音质量已经够用带宽占用只有 PCM 的十分之一。3.3 圆屏 UI 状态机设计圆屏是设备的“脸”交互状态的显隐直接影响用户对设备“聪明与否”的感知。我一开始没做状态机结果屏幕上的内容经常和实际动作错拍比如声音还在播屏幕已经回到待机界面了看起来特别蠢。后来我把 UI 重构成一个严格的状态机总共 6 个状态IDLE待机显示圆钟表盘和日期或者随机显示一句文案。LISTENING聆听中央显示实时音波动画四周一圈呼吸灯环表示设备在听。THINKING思考音波消失换成一个转圈的点阵动画文案显示“我琢磨一下”。SPEAKING回复显示回复文本字幕同时底部有一个音频进度条。ERROR异常显示错误图标和简短错误信息可以点击重试。OFFLINE离线显示“网络未连接”顶部有个小点提示重连中。用 C 语言定义一个枚举和切换函数typedef enum { UI_IDLE 0, UI_LISTENING, UI_THINKING, UI_SPEAKING, UI_ERROR, UI_OFFLINE } ui_state_t; void ui_set_state(ui_state_t new_state) { if (current_state new_state) return; current_state new_state; switch (new_state) { case UI_IDLE: lv_obj_clear_flag(ui_screen_idle, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(ui_screen_listening, LV_OBJ_FLAG_HIDDEN); break; case UI_LISTENING: lv_obj_clear_flag(ui_screen_listening, LV_OBJ_FLAG_HIDDEN); lv_anim_start(wave_anim); break; case UI_THINKING: // 隐藏聆听屏显示思考屏 break; case UI_SPEAKING: lv_label_set_text(ui_label_reply, last_reply_text); break; default: break; } }圆屏的 UI 布局和方形屏不一样四个角落很难利用所以我把主要内容都集中在中间一块圆形区域。信息层级从上到下顶部状态图标麦克风、脑袋、喇叭中间主动画区底部文字栏。LVGL 的显示缓冲区我设成 240x240x2 字节也就是一整个屏幕的分量。这样刷新效率最高但因为 PSRAM 够大这块缓冲直接放在 PSRAM 里不影响内部 RAM。实际跑下来LVGL 帧率能到 30fps 左右动画流畅度可以接受。3.4 断线重连、看门狗与低功耗兜底语音客户端最怕的一个场景是后台服务重启了或者家里路由器抽风设备直接卡死。用户对着屏幕喊半天一点反应没有特别劝退。所以我给设备加了四层兜底机制第一层心跳保活。设备每 5 秒发一个 ping 信令后台回 pong。如果 30 秒没收到 pong设备判定连接失效主动断掉旧连接并触发重连。这个间隔不能太短否则路由器老旧的时候会频繁掉线但也不能太长否则用户说完话才发现连接断了等待时间太久。第二层指数退避重连。重连尝试间隔从 1 秒开始失败后翻倍1s、2s、4s、8s……最多到 30 秒封顶。这能防止设备大规模离线后同时重连把后台打崩。第三层FreeRTOS 任务看门狗。每个关键任务录音任务、网络任务、UI 任务运行时给一个任务位喂狗。如果某个任务循环卡住超过 10 秒系统软复位。看门狗对语音设备的可靠性非常关键尤其是调试期一个死循环就能让设备变砖有它在能自动恢复。第四层网络断开降级。Wi-Fi 断开后UI 自动切换到 OFFLINE 状态停止录音和播放屏幕显示简洁的离线界面。同时后台线程尝试重连 Wi-Fi恢复后自动回到 IDLE。这套兜底机制在实际使用中救了很多次命。印象最深的是有一回后台服务因为上游模型接口超时被打爆客户端集体断开。由于有退避策略设备端没有同时冲击后台服务恢复后大家慢慢连回来用户基本无感知。4. 云端后台服务与模型衔接4.1 后台服务的整体职责后台是整个系统的“大脑”但它不是一个简单的代理转发。它需要处理的事情包括维护 WebSocket 连接接收设备音频流。将音频流转成文本ASR。把文本拼进对话上下文调用大模型得到回复。把回复文本合成语音TTS以音频流回传设备。管理会话状态、超时、异常重试、日志记录。我选了 Python 的 FastAPI 来做后台主要原因是 WebSocket 支持成熟而且和 ASR、大模型、TTS 的 SDK 生态结合得最好。设备接入层用 FastAPI 起一个 WebSocket 端点数据通道和信令通道共用同一个接口。后台主流程大概是这样的app.websocket(/ws) async def websocket_endpoint(websocket: WebSocket): await websocket.accept() session_id str(uuid.uuid4()) audio_buffer bytearray() # 接收音频信令按类型处理 while True: message await websocket.receive() if message[type] text: data json.loads(message[text]) if data[type] start_stream: audio_buffer.clear() elif data[type] end_stream: text await asr_recognize(bytes(audio_buffer)) if text: await websocket.send_text(json.dumps({type: reply, text: text})) elif message[type] bytes: audio_buffer.extend(message[bytes])实际实现里我把 ASR、LLM、TTS 都做成了可替换接口。今天用云厂商的 ASR明天可以换成自己部署的 Whisper后天可以把 TTS 换一个音色都只需要改配置不影响设备端协议。4.2 与 ASR、LLM、TTS 的对接方式先说 ASR。我这边用的是通用的流式识别方案直接把设备端上传的 PCM 数据按帧送入识别器。常见的做法有两种一种是 REST API 一次性上传整个音频适合短句另一种是 WebSocket 流式上传适合自然对话。糖球用的是流式因为用户说话的过程中我就希望后台开始识别而不是等用户说完再上传那样延迟会高很多。再说 LLM。后台调用大模型用的是 OpenAI 兼容接口这样不管后面接的是哪个模型服务只要对方提供 OpenAI 风格的 HTTP 接口我只需要改 base_url 和 api_key 即可。语音助手的系统提示词我写得很明确你是一个桌面语音助手名字叫糖球。 你的回复要简短、口语化控制在60字以内。 不要使用markdown语法。 如果用户问天气默认指广州市。 不要重复用户的问题。系统提示词很关键直接影响用户体验。最初我让模型自由发挥结果它每次回复都来一段长篇大论TTS 播半天没播完屏幕字幕都滚了好几页。后来把“简短”写进提示词才算是调教出了语音助手的味道。然后是 TTS。TTS 也是走云端接口生成结果返回的是音频字节流。第一版我为了简单等整段 TTS 生成完才把音频发回设备结果用户说一句话要等 3 秒才听到回复体验很差。后来改成流式合成模型每生成一句文本就交给 TTS 合成并立即下发设备端边收边播首句延迟从 3 秒降到 1 秒左右体感好了很多。4.3 多轮会话与记忆管理语音交互天然是多轮的。用户说“今天天气怎么样”紧接着说“明天呢”这个“明天”必须关联到前文的“天气”才能理解。所以后台必须维护会话上下文。我的做法是给每个设备分配一个 session_id在内存里存一个会话字典保存最近 10 轮用户和助手的消息。每次调用 LLM 时把历史会话按时间顺序拼成 messages 数组传进去。SESSIONS {} def build_messages(session_id: str, text: str): history SESSIONS.get(session_id, []) history.append({role: user, content: text}) # 只保留最近10轮 messages history[-20:] messages.insert(0, {role: system, content: SYSTEM_PROMPT}) return messages这个缓存放在进程内存里勉强够用但如果后台要横向扩展多个实例就需要换成 Redis。会话管理对语音客户端特别重要因为用户不会像用网页一样一个字一个字打而是随口说后台如果每次都“失忆”对话体验会非常差。上下文长度也要控制。大模型的输出 token 上限是固定的如果把历史全塞进去很容易在回答到一半时触达上限出现“回答被截断”的尴尬局面。我后来给上下文做了一个按字符数的截断保护如果历史消息总字符数超过 3000就只保留最近 5 轮。5. 常见问题与排查技巧实录5.1 麦克风无声或音量极低这个是最容易遇到的问题我调试时几乎每个新人都会卡一关。典型表现设备开机屏幕显示正常唤醒词也能触发但后台收到的音频全是静音或几乎听不清。排查思路按顺序来第一步先确定 I2S 有没有读到数据。在录音任务里加一行调试日志打印每次读到的字节数和第一个采样值。如果采样值一直为 0说明 I2S 根本没有数据进来。第二步检查 INMP441 的 L/R 脚。INMP441 的 L/R 引脚接高电平选右声道接地选左声道。如果你读的是左声道但 L/R 接了高电平读到的数据全是 0。这个坑特别隐蔽因为接反了不会报错只是没声音。第三步检查 I2S 的 channel_format。如果麦克风输出的是单声道但 I2S 配置成了双声道读取有一半通道是空数据平均音量会减半。比较稳妥的做法是 channel_format 用 RIGHT_LEFT读取后只保留对应声道的数据。第四步看电源纹波。如果麦克风 VDD 是直接从 ESP32-S3 的 3.3V LDO 拉的当 Wi-Fi 发送数据时电流波动麦克风会捕捉到明显的“嗡嗡”声。解法是在麦克风 VDD 引脚旁边加一个 10uF 钽电容和 0.1uF 陶瓷电容并联离引脚越近越好。音量低的问题我后来在后台做了动态增益。如果检测到人声峰值低于某个阈值就自动放大音频信号。这个对口语化交互很实用因为不同用户说话的音量差异太大了。5.2 圆屏刷新卡顿音频出现爆音圆屏用的是 SPI 接口I2S 也用 DMA 做音频收发两者同时工作会抢占总线。表现是一旦开始说话屏幕动画就掉帧或者一播 TTS麦克风采集的音频就带爆音。我用三个手段解决第一SPI 和 I2S 分引脚分总线。如果条件允许屏幕用 SPI2麦克风和功放用 I2S0互不干扰。如果只有一条 SPI 总线可用至少把屏幕刷新率降到 30Hz减少总线占用。第二LVGL 缓冲区分两半。LVGL 支持双缓冲刷新一个缓冲在后台写入另一个缓冲通过 DMA 送出DMA 刷新过程中不让 CPU 参与显著减少对音频任务的干扰。第三音频播放任务优先级调到高于 UI 任务。播放 TTS 时如果 UI 动画卡一下用户感知不强但音频卡顿用户立刻觉得设备坏了。任务优先级排序音频播放 音频采集 网络发送 UI 刷新。5.3 接入后台后频繁断连设备接入后台后频繁断连多半不是后台挂了而是设备端网络栈出了问题。现象是 WebSocket 连接建立后过几分钟就自动断开日志里报各种内存不足或者 LWIP 错误。排查经验先看内存峰值。ESP32-S3 内部 SRAM 只有 512KB被 Wi-Fi 协处理器和协议栈占据一部分后剩下给应用的不到 300KB。WebSocket 收发缓冲、TLS 握手缓冲、I2S DMA 缓冲都要吃内存如果内存不足连接会被系统强制断开。我建议把 WebSocket 的发送/接收缓冲区设成 4KB够用就行不要贪大。再查 LWIP 配置。LWIP 的 TCP_WND 和 TCP_SND_BUF 决定了 TCP 滑动窗口大小默认值可能偏小。在 sdkconfig 里手动调大这两个值音频流传输会更稳定。注意改完后要重新编译并烧录。还有一个坑是设备端 WebSocket ping/pong 间隔和后台不匹配。设备发 ping 太频繁后台以为异常后台不响应 pong设备又以为连接断了。两边约定好设备和后台都严格用 5 秒 ping、30 秒超时的参数问题就消失了。5.4 大模型输出被截断、回答不完整有段时间用户反馈糖球回答到一半突然停了屏幕字幕还在但 TTS 声音没了。后来查日志发现是大模型接口返回了 token 上限回答被截断。这个问题的根源在于大模型接口的 max_tokens 参数太小。语音助手场景下用户问一句话模型需要输出完整回答如果 max_tokens 设成 256遇到稍微复杂的问题就截断了。我把 max_tokens 调到了 1024问题基本解决。另外后台在做文本处理时一定要把大模型返回的 markdown 语法去掉。比如模型可能回复“好的我来帮你查一下。\n\n根据最新天气数据…”这里的 markdown 星号会原样播报出来用户听到“星号”两个字一脸懵。我在后台加了一个纯文本过滤函数把 markdown 符号全部剥离再把多余换行压缩成句号停顿。最后一点经验TTS 合成前要检查文本长度。如果模型输出的文本超过 200 字语音助手的体验已经崩了。这种情况下直接把文本截断或要求模型重写都比硬播完要好。最后再分享几个可以继续扩展的方向糖球做到现在最核心的架构已经稳定了ESP32 圆屏只负责采集、显示和播放所有模型推理都交给云端后台。这个“端云分离”的思路我认为很适合大多数桌面智能设备。后续我计划做三件事一是把离线的唤醒词进一步优化让设备在纯待机状态下电流降到 20mA 以下二是在后台接入多设备管理让家里的几个糖球可以共享一套会话记忆三是试试把分钟级的音频缓存放到 PSRAM 里断网的时候先本地记录恢复网络后再补传后台。如果你也在做类似的 ESP32 语音设备建议先跑通流程再优化细节。第一步只做“录音上传 文本回显”第二步加上 TTS第三步再完善 UI 和断线重连。每一步都不要跳过因为每层的坑都得亲手踩一遍才能长记性。有问题欢迎在评论区交流。
返回列表