跳到正文
Hacker News· tlack·· 8 天前AI 评分50

BabyTalk:ESP32 上的离线语音转文字与文字转语音

Show HN: Babytalk: Offline speech to text and text to speech on ESP32

AI 导读

BabyTalk 让 ESP32-S3 或 ESP32-P4 在无云端、无网络的情况下实现通用英语语音识别与语音合成,支持 C、MicroPython 及 AtomVM 上的 Erlang/Elixir。

正文 · AI 翻译

BabyTalk: small AI models for speech to text and text to speech on ESP32 microcontrollers

BabyTalk 让 ESP32-S3 或 ESP32-P4 听懂你说的话并回话,无需云端、无需联网,可用 C、MicroPython 或 AtomVM 上的 Erlang/Elixir 编写:

import stt, tts
text = stt.transcribe(pcm)                 # 16 kHz audio -> "what's the weather like today"
audio = tts.say("You said: " + text)       # text -> 24 kHz audio for the speaker

内存,关键结论:语音转文字需要 6 MB flash 和 84.5 KB 内部 RAM;文字转语音额外需要 1.9 MB flash。两者在运行时都会借用几百 KB 的 PSRAM。

flash PSRAM 内部 RAM
语音转文字(4-bit 模型) 6.0 MB:模型,原地读取 0.2 MB,转写 4 秒(更长片段需要更多) 84.5 KB,与文字转语音共用
文字转语音(nano 语音) 1.9 MB:语音 0.34 MB,发音 1.5 MB 0.5 MB,一段 4 秒的回复 同样的 84.5 KB
  • 语音模型绝不会被复制到 RAM:芯片在运行时从 flash 读取它。
  • 两者轮流使用一块 84.5 KB 的内部 RAM,而这块内存也可以放在 PSRAM 中(稍慢一些)。在 MicroPython 下,文字转语音还会在内部 RAM 中保留 104 KB 缓冲区;AtomVM 固件则把它们放在 PSRAM 中。
  • 更大更好:8-bit 语音模型(9.8 MB flash,更准确),以及在 ESP32-P4 上音质好得多的 heart 语音(2.4 MB flash,加载到 PSRAM)。所有选项:模型。完整明细:性能与内存。

由 Thomas Lackner 和 Claude Opus 5.5(Anthropic)制作。MIT 许可;语音模型归其作者所有(见 致谢与许可)。

这适合你吗?

大多数“ESP32 上的语音”项目只做两件事之一:

  • 把音频发送到云服务(Whisper API、Google、wit.ai)并等待文本,或者
  • 识别预先训练好的固定命令或唤醒词列表(例如 ESP-SR 的 MultiNet、Edge Impulse / TensorFlow Lite Micro 关键词识别)。

BabyTalk 不同:它是一个通用英语语音识别器,完全在芯片上运行。它能转写任意句子,无需训练,也没有命令列表。唤醒短语就是你输入的任何短语(“wake up, tomato face”),同样无需训练。可选地,同一固件还能用设备端神经网络语音说出回复。

如果你需要离线仍能工作、保持音频私密,或不能依赖服务器的语音输入,就会想要它。例如:

  • 没有键盘的无线电设备。一个 LoRa 节点(Meshtastic 或你自己的协议)有麦克风但没有键盘:说出消息,节点将其作为文本发送。一句话变成几十字节的文本;即使是非常低比特率的语音编解码器(700 bit/s 的 Codec2)也需要大约十倍于此,这在每秒最多传输几百字节的链路上很重要。
  • 没有屏幕的扬声器。接收节点朗读收到的文本消息。
  • WiFi 不可用时也能工作的语音控制,并且从不把音频发送到任何地方。

如果你需要非常高的准确率、英语以外的语言,或者它必须运行在更小的芯片上(见下文),那就跳过它。

(这也可能是唯一同时涉及 LoRa 无线电和 LoRA(低秩适配)的项目。docs/historical/ 中的原始研究计划探索了用于语音模型的 LoRA 风格专家适配器。它们尚未被使用。)

你需要什么

  • An ESP32-S3 or ESP32-P4 with 16 MB flash and 8 MB PSRAM (e.g. an S3 "N16R8" module). The P4 transcribes about 1.7 times faster (2 s of speech in 0.30 s, against 0.51 s) and can run a larger, better-sounding voice. BabyTalk runs on:
    • Waveshare ESP32-S3-CAM(ES7210 麦克风,ES8311 扬声器编解码器):参考板; 所有测量都在它上面进行,涵盖 MicroPython 和 AtomVM (docs/BOARD_WAVESHARE_S3_CAM.md)
    • Waveshare ESP32-P4-WIFI6(ES8311 麦克风和扬声器编解码器),来自 AtomVM (docs/BOARD_WAVESHARE_P4_WIFI6.md)
    • LilyGO T-LoRa Pager(ES8311),来自 AtomVM,带有自己的音频驱动
    • Elecrow CrowPanel Advance 7.0"(PDM 麦克风),来自 AtomVM,仅语音转文本
  • 一个麦克风,用于语音转文本:任何能提供 16 kHz PCM 的设备均可。
  • 一个扬声器,用于文本转语音(可选)。
  • 构建所需:Linux 或 WSL、ESP-IDF v5.5.1, 以及用于 Python 工具的 uv。(AtomVM 还需要 Erlang/OTP 26+ 和 Elixir 1.17+。)

模型

语音转文本

NVIDIA 的 Citrinet-256,压缩 至 4 位或 8 位,有两个版本:原始版本,以及一个针对背景噪声微调的版本。 四个模型均可从 models/ 直接烧录(SHA-256、许可证及每个模型的制作方式: models/README.md)。

文件 烧录 嘈杂房间* 干净语音** 适配
citrinet256_int4.mmrt(默认) 6.0 MB 58.1% 8.2% 标准的 6 MB 模型分区
citrinet256_int8.mmrt 9.8 MB 51.5% 6.3% 一个 10 MB 模型分区
citrinet256_noisy_int4.mmrt 6.0 MB 42.7% 11.8% 标准的 6 MB 模型分区
citrinet256_noisy_int8.mmrt 9.8 MB 33.8% 7.8% 一个 10 MB 模型分区

词错误率,越低越好。*82 段在开发板上录制的录音,背景中有音乐、高速公路 噪声或咖啡店循环声(参见 为真实世界训练)。**LibriSpeech test-clean(干净、 朗读英语);原始全精度模型得分为 3.8%。

noisy 模型是替代方案,而非升级:房间有噪声时错误词数减少四分之一到三分之一,干净语音上错误更多。根据开发板 将在何处聆听来选择。它们与原始模型大小相同,无需更改固件或代码: 文件会记录自己的格式。

唤醒词无需自己的模型:语音模型的输出会与短语进行评分比对。

文本转语音

语音 烧录 PSRAM 音质* 运行于
nano(默认),294k 参数 含发音词典为 1.9 MB 0.5 MB 2.13 所有开发板,编译进固件
heart,2.27M 参数 额外 2.4 MB,位于自己的分区 额外 2.4 MB 3.36 仅 ESP32-P4,半实时速度

*SCOREQ,一种预测的听者评分(越高越好)。两者都是 sanoTTS 语音;详情及 4 位 heart4: docs/TTS_VOICES.md。

构建你自己的(在你的录音上微调、其他语言、其他语音): docs/MODELS.md。

快速开始

三种方式共用同一个引擎(components/),并给出相同的转录结果。

MicroPython(ESP32-S3)

一个带有 stt 和 tts 模块的 MicroPython v1.27.0 固件。你通过 machine.I2S 提供音频;ES7210 和 ES8311 的编解码器驱动位于 mpy/drivers/。转录 可以在 asyncio 继续运行时在后台进行,唤醒词是一个 拼写列表。完整 API:mpy/README.md。

import stt
stt.phrase(["wake up tomato face"])        # score every run for this phrase
text = stt.transcribe(pcm)
stt.last()["score"]                        # 0 = the phrase is the best reading

1. 构建固件(MicroPython v1.27.0,带有一个小补丁,让 machine.I2S 驱动编解码器的主时钟):

REPO=$PWD                                   # this repository
. ~/esp/esp-idf/export.sh                   # wherever your ESP-IDF v5.5.1 lives
mkdir -p ~/build/sentry-fw && cd ~/build/sentry-fw
git clone --depth 1 -b v1.27.0 https://github.com/micropython/micropython
git -C micropython apply $REPO/mpy/patches/micropython-i2s-mck.patch
make -C micropython/ports/esp32 submodules
git clone https://github.com/Ampixa/sanoTTS ~/build/tts/sanoTTS             # optional: text to speech,
git -C ~/build/tts/sanoTTS checkout 18e26b2b365bff41d211e516b0760021451438f1  # at the audited commit
cd $REPO && mpy/build.sh

2. 烧录固件和一个模型,然后清空一次文件系统区域:

# a 6 MB model (default layout)
esptool.py --chip esp32s3 write_flash 0x0 ~/build/sentry-fw/out/firmware-stt.bin \
  0x410000 models/citrinet256_int4.mmrt
esptool.py --chip esp32s3 erase_region 0xA10000 0x5F0000

# or: a 9.8 MB model (int8 layout: same firmware, bigger model partition)
esptool.py --chip esp32s3 write_flash 0x0 ~/build/sentry-fw/out/firmware-stt.bin \
  0x410000 models/citrinet256_int8.mmrt
esptool.py --chip esp32s3 write_flash 0x8000 ~/build/sentry-fw/out/partition-table-int8.bin
esptool.py --chip esp32s3 erase_region 0xE10000 0x1F0000

3. 试一试:按 Enter,说话,开发板会复述它听到的内容。

uv run --with mpremote tools/echo.py

mpy/examples/ 还有一个唤醒词演示、一个扬声器演示和 model_tools.py(从 SD 卡安装 模型:docs/MODELS.md)。

AtomVM(Erlang 和 Elixir,ESP32-S3 和 ESP32-P4)

AtomVM v0.7 还没有 I2S 驱动,因此 BabyTalk 自带音频驱动(ES7210、ES8311、 一个由 GPIO 或扩展器切换的功放)。引脚和器件在运行时选择,因此每个芯片只有一个 固件,你的应用可以选择板级预设或自己的引脚 (boards and pins)。它增加了一个受监督的 gen_server, 用于监听唤醒短语并发出可听见的提示音,以及 MMRT 作为独立的 int8/int4 向量内核库(matvec、matmul、top-k……),可用于任何 AtomVM 项目。

ok = babytalk:audio_config(waveshare_s3_cam),       % or your own pins: a map
{ok, Pcm} = babytalk:record(3),
{ok, Text, _Info} = babytalk:transcribe_sync(Pcm, 10000),
ok = babytalk:speak([<<"You said: ">>, Text]).

构建、烧录和演示应用:atomvm/README.md。

C(ESP-IDF)

将 components/stt_engine(以及 mmrt 和 sram_pool)添加到你的 ESP-IDF 项目中,对于语音, 添加 components/sanotts。引擎从 model 闪存分区映射模型;你向它提供 16 kHz PCM。

stt_engine_open();
stt_result_t r;
stt_engine_run(pcm, n_samples, &r);         // r.text: "what's the weather like today"
int16_t *out; int n_out;
tts_say(r.text, 0.8f, &out, &n_out, NULL);   // 24 kHz PCM (in PSRAM) for your I2S driver

设置、构建选项和完整 API:components/README.md。 单独的推理运行时:mmrt/README.md。

工作原理

mic -> 16 kHz audio -> log-mel features -> Citrinet-256 (neural net) -> letters/word pieces -> text
                                                                      \-> wake phrase score
text -> pronunciation dictionary -> phonemes -> sanoTTS voice -> 24 kHz audio -> speaker
  • 语音转文本使用 NVIDIA 的 Citrinet-256,这是一个在 960 小时英语有声读物上训练的 980 万参数识别器。它直接输出文本(“CTC”),因此 无需维护词汇表。
  • 唤醒短语:同一模型输出会根据你的短语拼写进行评分,因此 任何短语都可用。export/kws_validate.py 会将短语与数小时的其他语音进行比对, 以选择一个避免误唤醒的阈值。
  • 文本转语音使用 sanoTTS 语音。它是可选的:固件可以在没有它的情况下构建。

MMRT,运行时

该模型运行在 MMRT(mmrt/,“micromodels runtime”)上,这是一个为 BabyTalk 编写的小型推理引擎, 旨在以后承载其他小型模型。 我们从 Espressif 的 ESP-DL 开始。它给出了正确的结果,但处理 每一秒音频需要 1.33 秒,因为它对每一帧音频都从闪存重新读取每一层的权重, 并且只使用一个核心。修复这一点意味着要给 ESP-DL 打补丁,而其他人很难轻松 构建它,所以我们自己写了一个。MMRT 比原版 ESP-DL 快约 13 倍,体积只有其一半, 输出完全相同:

  • 它从闪存就地读取模型;启动时不会将任何内容复制到 RAM。
  • 它将每一层的权重复制到快速内部 RAM,每层一次,而不是每帧一次, 并将工作拆分到两个核心上。
  • 它的内核是手写汇编,针对 ESP32-S3 的向量指令, 每条指令执行 16 次乘加。
  • 它融合层,使中间结果保留在内部 RAM 中,并且在短片段上 将复制下一层权重与计算当前层重叠进行。
  • 在其他芯片上,它有可移植的 C 内核,在 ESP32-P4 上则有针对该芯片 向量单元的内核。
  • 它加载8 位或 4 位权重,并且每个内核都会与 纯 C 版本逐位测试。

细节、一个 C 使用示例以及每个设计选择背后的测量数据: mmrt/README.md。

量化:将 39 MB 模型装入 6 MB

原始模型使用 32 位浮点数(约 39 MB)。它被压缩两次:

  1. int8:权重和激活值变为 8 位整数(9.8 MB)。
  2. int4:每个输出通道将 16 个权重值保存在一个小查找表中,通过 GPTQ(一种误差补偿方法)选择。 第一层和最后一层保持 8 位。 结果:6.0 MB。

每一步都会损失一些准确率(在干净语音上词错误率 3.8% -> 6.3% -> 8.2%;参见 Models)。自己从 NVIDIA 的检查点或你自己 微调过的检查点重建它们:docs/MODELS.md。

什么在哪里

文件夹 内容
mmrt/ int8/int4 推理运行时(C + ESP32-S3 和 ESP32-P4 SIMD 汇编)
components/ 引擎作为 ESP-IDF 组件,供 C 应用和两个固件使用:mmrt、stt_engine、sram_pool、sanotts(README)
mpy/ MicroPython 固件:stt 和 tts 模块、编解码器驱动、示例
atomvm/ AtomVM(Erlang/Elixir)固件:NIF、库、演示应用
stt/ 一个纯 ESP-IDF 测试固件,引擎在此开发和测量
bench/ ESP32-S3 微基准测试(内存、内核、麦克风)
export/ PC 端:模型移植、量化、.mmrt 导出器、位精确校验
train/、field/、datagen/ 微调、现场录音会话、合成语音
tools/ 通过 WiFi 或 USB 驱动开发板的笔记本电脑工具(转写、录音、唤醒词)
models/ 语音模型,可直接烧录
docs/ MODELS.md(使用、构建和替换模型;其他语言)、ROADMAP.md、BENCHMARKS.md(ESP32-S3 的内存和运算速度、术语表)、开发板说明、项目历史

性能和内存

在 Waveshare ESP32-S3-CAM 上测量,使用 MicroPython,4 位模型。时间包含音频特征提取步骤。

速度

任务 时间
转写 1 秒语音 0.35 秒
转写 2 秒语音 0.51 秒
转写 4 秒语音 0.83 秒
转写 10 秒语音 1.72 秒
转写 1 秒,使用 6 MB 权重缓存 0.31 秒
转写 4 秒,使用 6 MB 权重缓存 0.79 秒
朗读 1 秒语音 0.26 秒

可选的权重缓存(stt.cache(6144))几乎不剩多少空闲 PSRAM,因此启用后,在 MicroPython 下音频片段被限制在约 4 秒。

闪存(16 MB)

项目 大小
语音模型,4 位 6.0 MB
语音模型,8 位(int8 布局) 9.8 MB
文本转语音:nano 音色 0.34 MB,发音词典 1.5 MB 1.9 MB
MicroPython 本身,含摄像头模块 1.7 MB
整个 MicroPython 固件(MicroPython + 文本转语音) 3.6 MB
留给你的文件,4 位布局 6 MB
留给你的文件,int8 布局 2 MB

PSRAM(8 MB)

项目 大小
空闲时、语音已加载时的可用量 7.9 MB
转写 4 秒时使用 0.2 MB
4 秒语音回复使用 0.4 MB
可选权重缓存 6 MB

内部 RAM(稀缺的那个)

项目 大小
监听/朗读的共享块(它们轮流使用) 84.5 KB
文本转语音,常驻缓冲区(AtomVM 下位于 PSRAM) 104 KB
留给你的程序 ~42 KB

此固件中蓝牙被禁用以腾出这些空间。在 AtomVM 下让 BabyTalk 与 WiFi、摄像头或屏幕共存(将共享块和任务栈移到 PSRAM、减少 WiFi 缓冲区):atomvm/README.md。芯片的原始内存和运算速度:docs/BENCHMARKS.md。

面向真实世界的训练(进行中)

语音模型从干净的有声书学习。在书桌前它表现良好,但背景中有发动机、音乐或人说话时,它就崩了:在我们录制的柴油卡车怠速背景上,即使全精度模型也有大约一半的词出错。因此我们在开发板上录制真实世界的音频,并据此微调模型。

录音(field/):一次会话播放一种背景声音(电视、咖啡店循环音、卡车发动机,或真实环境),并在屏幕上显示句子。你逐句朗读,然后笔记本电脑以约 35 种合成音色之一、随机速度朗读同一句子,开发板通过 WiFi 将两者都录下。句子来自有声书、今日新闻、热门维基百科文章、Hacker News 头条,以及你自己的词表(field/terms.txt:人名、命令、术语)。仅背景的录音也会保存,用于混入训练。

免手操作:使用通过 USB 流式传输麦克风的开发板(atomvm/apps/mic_stream,例如在 LilyGO T-LoRa Pager 上),capture.py --link usb --auto 只录制笔记本电脑的声音,无需提示,因此可以在驾驶过程中运行会话。笔记本电脑根据播放开始的位置(通过互相关找到)从流中切出每个片段,并保留整个流。

测量(export/field_eval.py):一条命令即可对录音中的每个模型进行评分。五分之一的句子保留用于测试,从不用于训练。export/hard_words.py 会持续记录模型出错的单词列表,录音器可以针对这些单词(--hard)或重做它错过的句子(--retake)。

训练(train/finetune.py,在笔记本电脑 GPU 上约 15 分钟;没有 GPU 时 --device cpu 也能工作,但需要约 1.5 天):模型持续从 100 小时的有声读物、现代文本的合成语音以及现场录音中学习,并混入录制的背景噪声和其他损伤(闷音、削波、丢帧)。

目前的结果:Models 中的 noisy 模型。在 82 段模型从未听过的录音中,全精度模型将错误单词率从 47.2% 降至 30.8%,开发板上的 4 位模型从 58.1% 降至 42.7%;在干净的有声读物上,它们损失了一些准确率。保持干净语音准确率的训练是下一步(docs/ROADMAP.md)。

cd field && uv run prompts.py                                   # build the sentence pool
uv run capture.py --condition truck-idle --noise "diesel idling"   # a recording session
uv run capture.py --link usb --board lilygo-t-lora-pager --mic es8311 --auto --minutes 45 \
    --condition truck-drive --noise "diesel, open doors, traffic"     # hands free
cd ../export && uv run field_eval.py --split all                # score the models
cd ../train && uv run make_tts.py && uv run finetune.py         # fine-tune (GPU if present; --device cpu|mps|cuda)

为开发板转换结果:docs/MODELS.md。

局限性

  • 仅限英语,16 kHz 音频。转录在你停止说话后开始(无实时流式传输),并且在麦克风附近一次只有一位说话者时效果最佳。
  • 背景噪声仍然代价很大:noisy 模型在我们嘈杂的录音中会弄错三分之一到五分之二的单词,并且为此牺牲了一些干净语音的准确率。
  • 4 位模型以约 2 个百分点的准确率换取更小的体积(见 Models)。
  • 微小的声音很清晰,但明显是合成的,并且会读错一些单词。
  • MicroPython 在一块开发板(Waveshare ESP32-S3-CAM)上经过测试,AtomVM 在 What you need 中的四块上经过测试。对于 AtomVM,另一块具有相同音频芯片(ES7210/ES8311)的开发板只需要其引脚,在运行时设置(babytalk:audio_config/1);其他芯片需要 C 驱动程序(board_audio.h)。MicroPython 的驱动程序(mpy/drivers/)在你脚本打开的 I2C 总线和 I2S 端口上工作,因此它们的引脚已经由你选择。
  • 听和说轮流进行(它们共享一块内部 RAM)。
  • 在 MicroPython 下,WiFi 开启时语音转文本无法运行:WiFi 占用了它所需的内部 RAM(剩余 6.7 KB)。AtomVM 固件在 WiFi 开启时保留约 42 KB,可以运行。

未来工作

更完整的列表,以及每项所需的条件:docs/ROADMAP.md。

  • 保持干净语音准确率的微调,以及模拟开发板的 8 位和 4 位算术进行训练(量化感知训练),以赢回量化损失的部分。
  • 在更多地点和声音中录制,并在目标设备(LilyGO T-Watch S3 Plus)上录制。
  • 在你还在说话时进行转录(流式传输)。
  • 在 S3 上实现更好的声音:从更大的 TTS 模型蒸馏出的声音(在 ESP32-P4 上,sanoTTS 的更大 heart voice 已经以半实时运行:docs/TTS_VOICES.md)。
  • 释放更多内部 RAM,以便摄像头、蓝牙和语音可以同时运行。
  • 预构建固件下载;更多开发板;更多语言。

另请参阅

  • ESP-SR:乐鑫的唤醒词(WakeNet)和固定命令识别(MultiNet),适用于 ESP32-S3。
  • ESP-DL:乐鑫的通用神经网络库,适用于 ESP32 芯片。
  • TensorFlow Lite Micro "micro_speech" 和 Edge Impulse:训练你自己的关键词识别器。
  • sanoTTS:这里使用的小型神经 TTS (Arduino、ESPHome、浏览器)。
  • TinyTTS 和 rvTTS:其他在微控制器上运行的神经 TTS。
  • whisper.cpp:面向 PC、手机和单板计算机的 Whisper 语音识别 (对 ESP32 来说太大)。
  • Meshtastic:开放 LoRa 网状消息通信。
  • NVIDIA NeMo:Citrinet 的来源。

致谢与许可

BabyTalk 由 Thomas Lackner 和 Claude Opus 5.5(Anthropic 的 AI 模型) 共同合作完成。

本仓库中的代码以 MIT 许可证发布,版权归 Thomas Lackner 所有。此处使用的他人作品保留其各自的许可证:

  • 语音模型(models/):衍生自 NVIDIA 的 stt_en_citrinet_256_ls, CC-BY-4.0。由本项目转换和量化,noisy 模型经过 微调;参见 models/README.md。
  • 文本转语音:sanoTTS,由 Ampixa 开发,在构建时从你自己的检出中获取 (固定提交)——本仓库中不包含其任何内容。其运行时 采用 MIT 许可,其词典(来自 misaki)采用 Apache-2.0,但我们编译的三个文件 没有自己的许可证,属于 sanoTTS 的 GPL-3.0 默认条款,因此包含文本转语音的 固件二进制文件应被视为 GPL-3.0,直到上游 澄清此事。逐文件详情:components/sanotts/LICENSES.md。
  • MicroPython(MIT)和 ESP-IDF(Apache-2.0),在构建时下载; 固件还编译了 Espressif 的 esp-dsp(Apache-2.0,即 log-mel 前端的 FFT)。bench/ 固件使用 esp-nn(Apache-2.0)。
  • AtomVM(Apache-2.0 或 LGPL-2.1-or-later):该 VM 及其 Erlang/Elixir 标准 库(boot.avm)是 AtomVM 固件镜像(atomvm/)的一部分。在 构建时下载;我们的本地补丁位于 atomvm/patches/。
  • 音频编解码器驱动:mpy/drivers/、 stt/main/mic.c、bench/main/bench_rec.c 和 atomvm/components/atomvm_babytalk/ board_audio.c 中的 ES7210 和 ES8311 寄存器序列 衍生自 Espressif 的 esp-bsp 驱动(Apache-2.0, licenses/Apache-2.0.txt);每个文件都注明了这一点。
  • 1x1 卷积内核的部分内容改编自 Espressif 的 ESP-DL(MIT)。

项目历史和早期设计笔记:docs/historical/。

来源:Hacker News · github.com