OfflineSpeech 离线语音识别拓展
完全离线的语音识别扩展:无需联网即可识别中文等多种语言,支持实时麦克风识别和音频文件识别。底层采用业界权威的开源离线识别引擎 Vosk(Kaldi)。
默认中文小模型开箱即用(vosk-model-small-cn-0.22,zip 约 42MB,Apache 2.0 开源协议):调用 LoadDefaultModel 即可——本地已有缓存就直接加载;没有就首次联网自动下载并缓存(进度经 ModelProgress 事件提示),之后全程离线。日常语音指令、字幕转写完全够用。需要更高识别精度或其它语言时,改一个 DefaultModelUrl 地址即可换装大模型。
为什么模型不打进拓展包:Vosk 中文小模型 zip 有 42MB,塞进 aix 会让拓展包膨胀到 44MB+,导入工程后连 APK 都编译不了(超出平台工程文件上限)。所以 v3.0 起模型走「首次下载 + 本地缓存」,aix 只有 6MB。
升级说明:v1.0 的
LoadBundledModel报「当前扩展未内置模型」;v2.0 曾把模型内置进 aix(44MB,会卡死编译,已撤回)。请删除旧拓展,重新导入本页的 v3.0 aix。LoadBundledModel块在 v3.0 仍保留、效果等同LoadDefaultModel,老工程不用改块。
打包架构:arm64-v8a + armeabi-v7a(覆盖绝大多数真实安卓手机)。
- .aix 拓展下载:
快速上手:3 分钟做一个实时字幕器(案例①)
成果:点「开始说话」对着手机讲话,屏幕上实时滚出文字;点「停止」结束。全程不联网。
器材:一台安卓手机(联网只需两次:第一次从本站下载拓展、第一次启动时下载模型约 42MB)、AI 伴侣或打包 APK 均可。
- 导入拓展:设计视图 → 扩展 → 导入拓展 → 从本页下载的 aix 导入。
- 搭界面:从组件面板拖入——
标签×2:改名标签_状态(提示语,字号 14)、标签_字幕(字幕,字号 20);按钮×2:改名按钮_开始(文本「🎤 开始说话」)、按钮_停止(文本「⏹ 停止」);- 非可视组件
OfflineSpeech1(扩展分类里)。
- 连代码块:
when Screen1.Initialize() {
标签_状态.Text = "正在准备模型…"
OfflineSpeech1.LoadDefaultModel()
}
when OfflineSpeech1.ModelProgress(message) {
标签_状态.Text = message
}
when OfflineSpeech1.ModelLoaded() {
标签_状态.Text = "模型就绪,点「开始说话」"
按钮_开始.Enabled = true
}
when OfflineSpeech1.Error(operation, message) {
标签_状态.Text = join("出错(", operation, "):", message)
}
when 按钮_开始.Click() {
标签_状态.Text = "正在听…"
OfflineSpeech1.StartListening()
}
when 按钮_停止.Click() {
OfflineSpeech1.StopListening()
}
when OfflineSpeech1.PartialResult(text) {
标签_字幕.Text = text
}
when OfflineSpeech1.Result(text, raw) {
标签_字幕.Text = text
}
when OfflineSpeech1.FinalResult(text, raw) {
标签_状态.Text = "已停止"
}
- 跑起来看现象:
- 首次启动:
标签_状态依次显示「首次使用:正在下载中文模型(约42MB,只需这一次)…」→「模型下载中 N%…」→「正在解压默认模型…」→「模型就绪」(第二次启动直接就绪,不下载不解压); - 允许录音权限后点「开始说话」,讲一句「今天天气真好」——
标签_字幕先出现不太稳定的临时文字(PartialResult),停顿半秒后定格为最终文字(Result); - 飞行模式下重复一遍——照样能识别,这就是”离线”(识别本身从不需要网络,只有第一次下载模型要联网)。
- 首次启动:
怎么工作的:LoadDefaultModel 发现本地已有模型缓存就直接加载;没有就从 DefaultModelUrl 下载 zip 到应用目录、解压、加载(下载/解压进度经 ModelProgress 事件广播,只在第一次发生);StartListening 打开麦克风,识别引擎每猜一次就触发 PartialResult,句子讲完触发 Result;StopListening 收尾并触发 FinalResult。
案例②:声控灯——「开灯」「关灯」
成果:对手机喊「开灯」屏幕变亮黄色、喊「关灯」变深灰——语音命令的标准做法:在稳定结果里找关键词,而不是比较整句相等。
在案例① 的基础上加:标签 改名 标签_灯(文本「💡 亮」,宽高 200×120、字号 30、居中)、标签_识别(显示原句):
when 按钮_开始.Click() {
if (not OfflineSpeech1.ModelReady) {
标签_灯.Text = "模型还没就绪,先等 ModelLoaded"
} else {
OfflineSpeech1.StartListening()
}
}
when OfflineSpeech1.Result(text, raw) {
标签_识别.Text = text
if (textContains(text, "开灯")) {
标签_灯.Text = "💡 亮"
标签_灯.BackgroundColor = &HFFFFC107
} else {
if (textContains(text, "关灯")) {
标签_灯.Text = "🌙 灭"
标签_灯.BackgroundColor = &HFF37474F
}
}
}
现象与验证:喊「请把灯打开」也会亮(textContains 找的是关键词不是整句匹配);说「关门」不会误触发「关灯」吗?会——这就是为什么命令词要选区分度高的词(「开灯/关灯」比「开/关」好),也可以再加 textContains(text, "灯") 做双条件。
延伸:把「变色」换成 蓝牙客户端.发送文本 就是语音遥控;换成 TinyDB.保存数值 就是语音记账。识别不准时优先换更口语化的关键词,而不是急着换大模型。
案例③:识别工程素材里的 WAV 文件
成果:不用麦克风——把一段录音 wav 上传为工程素材,点按钮转写出全文。适合做”听写批改”“录音笔记”类应用,也是排查识别问题的好办法(输入固定,结果可复现)。
准备音频:必须是 16kHz、单声道、16bit PCM 的 WAV。用电脑上的 Audacity(导出 WAV 时选「采样率 16000、单声道」)或格式工厂转换即可。注意:AI2 自带的 SoundRecorder 组件录出的是 AMR 格式,不能直接给 RecognizeFile 用,需先转成上述参数的 WAV。文件大小要在素材上传限制内(≤15MB,约合 8 分钟)。
- 设计时把
test16k.wav上传为工程素材(素材面板 → 上传文件); - 搭
标签_全文(多行、高度充填)+ 代码块:
when Screen1.Initialize() {
OfflineSpeech1.LoadDefaultModel()
}
when OfflineSpeech1.ModelLoaded() {
标签_全文.Text = "正在识别 test16k.wav …"
OfflineSpeech1.RecognizeFile("test16k.wav")
}
when OfflineSpeech1.Result(text, raw) {
标签_全文.Text = text
}
现象:模型就绪后自动开始转写,几秒内 标签_全文 跳出文字(40 秒的录音大约几秒转完)。RecognizeFile 写文件名就行——拓展会先到应用目录找,再到工程素材里找,最后才当绝对路径处理,所以伴侣和打包 APK 用同一套块,不用关心各自的数据目录。
中途验证:故意把文件名写错成 test.wav → 标签_全文 应显示 Error 事件里的「找不到音频文件」提示——说明错误链路也是通的。
案例④:产品级换装模型——改一个地址,换英文/大模型
成果:默认的中文小模型不是唯一选择——启动时把 DefaultModelUrl 指到任何一个 Vosk 模型 zip 直链再 LoadDefaultModel,拓展就自动「有缓存秒开、没缓存下载」。「下载→缓存→换装」全在拓展内部闭环,连 Web 组件都不用。换 1.3GB 中文大模型也是同一套块,只改地址。
器材:手机需联网(仅首次下载模型时),界面沿用案例①:
global 目标模型zip = "vosk-model-small-en-us-0.15.zip"
when Screen1.Initialize() {
标签_状态.Text = "正在准备英文模型…"
OfflineSpeech1.DefaultModelUrl = join("https://alphacephei.com/vosk/models/", 目标模型zip)
OfflineSpeech1.LoadDefaultModel()
}
when OfflineSpeech1.ModelProgress(message) {
标签_状态.Text = message
}
when OfflineSpeech1.ModelLoaded() {
标签_状态.Text = "英文模型就绪,说句 hello 试试"
按钮_开始.Enabled = true
}
when OfflineSpeech1.Error(operation, message) {
标签_状态.Text = join("出错(", operation, "):", message, "(首次使用需联网下载模型)")
}
when 按钮_开始.Click() {
OfflineSpeech1.StartListening()
}
when OfflineSpeech1.Result(text, raw) {
标签_字幕.Text = text
}
分步看现象:
- 第一次启动:状态栏依次「正在下载中文模型…」(文案是通用提示)→「模型下载中 N%…」→「正在解压默认模型…」→「英文模型就绪」。此时讲 “hello world” 试试。
- 第二次启动:直接「英文模型就绪」——缓存已在本地,秒开、零流量。
- 断网重启:第 2 步照常成功;清掉应用数据再断网 → 触发
Error(提示首次使用需联网),界面据此给「请连网后重试」的引导即可。
要点:每个模型 zip 在应用目录各有一份独立缓存(按 zip 文件名区分),中文默认模型与英文模型互不覆盖——把 DefaultModelUrl 改回默认地址(或清空后重启应用)再 LoadDefaultModel 就换回中文模型,两个模型都已缓存时切换是秒级的。换装前拓展会自动停掉在跑的识别。
进阶:想自己控制下载界面(自定义进度条、静默后台)的,可以改走 Web 组件路线——
SaveResponse=真+ResponseFileName让 Web 把大文件流式写盘(不整包进内存),GotFile事件的fileName参数就是落盘后的绝对路径,直接喂给OpenModelZip(fileName)。
案例⑤:词级时间戳——每个词几点出声
成果:把 Result 的原始 JSON 解开,逐词列出「这个词从第几秒到第几秒」。做卡拉OK字幕、逐词高亮、录音审计都靠它。
打开 ShowWords 属性后,Result/FinalResult 的 raw 参数里多一个 result 数组,每项含 word(词)、conf(置信度 0~1)、start/end(秒)。在案例① 基础上加 Web1(借用它的 JSON 解析方法):
global 时间轴 = ""
when Screen1.Initialize() {
OfflineSpeech1.ShowWords = true
OfflineSpeech1.LoadDefaultModel()
}
when 按钮_开始.Click() {
时间轴 = ""
OfflineSpeech1.StartListening()
}
when OfflineSpeech1.Result(text, raw) {
标签_全文.Text = text
词表 = dictLookup(Web1.JsonTextDecodeWithDictionaries(raw), "result", [])
foreach 词 in 词表 {
时间轴 = join(时间轴, dictLookup(词, "word", ""),
"(", formatdecimal(dictLookup(词, "start", 0), 2),
" ~ ", formatdecimal(dictLookup(词, "end", 0), 2), " 秒)\n")
}
}
when 按钮_停止.Click() {
OfflineSpeech1.StopListening()
}
when OfflineSpeech1.FinalResult(text, raw) {
标签_时间轴.Text = 时间轴
}
现象:讲「你好世界」,停止后标签里出现 你好(0.31 ~ 0.55 秒)世界(0.62 ~ 0.98 秒) 这样的逐词清单;formatdecimal(…, 2) 把秒保留两位小数。逐词含 conf 置信度,低于 0.6 的词可以做「人工复核」标记。
模型怎么来:默认下载 / 换地址 / 手动放入
识别前必须有一个 Vosk 模型。三条路线按省事程度排序:
| 路线 | 适用 | 做法 |
|---|---|---|
| A. 默认模型(推荐) | 中文、零配置 | LoadDefaultModel()——有缓存秒开,没缓存首次联网自动下载,什么都不用准备 |
| B. 换装其它模型 | 英文等其它语言 / 更高精度 | DefaultModelUrl 设为模型 zip 直链 → LoadDefaultModel(),见案例④ |
| C. 手动获取 zip 后加载 | 不想消耗手机流量 / 电脑旁有 adb | Web 组件下载或 adb push 模型.zip /sdcard/Android/data/<包名>/files/ → OpenModelZip("模型.zip") |
路线 A/B 的默认地址:https://www.fun123.cn/static/models/vosk-model-small-cn-0.22.zip(本站镜像,国内快);DefaultModelUrl 留在默认值就是它。海外或自建镜像可换成 https://alphacephei.com/vosk/models/<模型名>.zip。
路线 C 细节:<包名> 在用 AI 伴侣联调时是 edu.mit.appinventor.aicompanion3,打包 APK 后是你自己的应用包名(可在「打包」结果页看到)。部分手机的文件管理器不允许写 Android/data,用 adb 按上面的完整路径 push 一般没问题;实在不行回路线 B。
不支持把模型 zip 上传为工程素材:素材上传限制 15MB,而最小的 Vosk 模型 zip 也有 36MB。(WAV 音频按案例③ 走素材没问题。)
模型一览(官方列表在 alphacephei.com/vosk/models,全部 Apache 2.0 协议可商用,全部 16kHz):
| 模型 | 语言 | zip 体积 | 特点 |
|---|---|---|---|
vosk-model-small-cn-0.22 |
中文 | 42MB | 默认模型(LoadDefaultModel 首次下载);解压后约 68MB,字错率约 23%,日常够用 |
vosk-model-cn-0.22 |
中文 | 1.3GB | 中文精度最高;需 2GB 以上运行内存,低端机慎用 |
vosk-model-cn-kaldi-multicn-0.15 |
中文 | 1.5GB | 早期多语料版,一般选上面那个即可 |
vosk-model-small-en-us-0.15 |
美式英语 | 40MB | 案例④ 的换装示例;与默认中文模型配合可做双语 |
vosk-model-small-en-in-0.4 |
英语(印度口音) | 36MB | 口音适配场景 |
下载地址规律:https://alphacephei.com/vosk/models/<模型名>.zip。
属性
- SampleRate
- 识别采样率(Hz),需与模型匹配,绝大多数 Vosk 模型为 16000(默认值)。
- ShowWords
- 是否在结果 JSON 中包含逐词时间戳与置信度(
result字段),见案例⑤。默认假。 - MaxAlternatives
- 返回的候选结果数量(0 表示只返回最可能的一个)。
- DefaultModelUrl
- 默认模型压缩包的下载地址(
LoadDefaultModel使用)。默认指向本站镜像的中文小模型;换成其它 Vosk 模型 zip 直链即可换装(见案例④),改完重新调用LoadDefaultModel。 - ModelReady
- (只读)模型是否已加载完成。加载完成后才能开始识别。
事件
- ModelLoaded()
- 模型加载完成时触发,此后即可开始识别。换装模型(
LoadDefaultModel/OpenModel/OpenModelZip)成功后同样触发。 - ModelProgress(message)
- 模型准备过程提示(下载百分比/解压/加载进度文案)持续触发,适合显示「模型下载中 45%…」之类的状态。
- PartialResult(text)
- 识别到中间(不稳定)结果时持续触发,text 为当前临时识别文本。
- Result(text,raw)
- 识别到一段稳定结果时触发。text 为识别文本,raw 为完整 JSON 结果(
ShowWords为真时含逐词信息)。 - FinalResult(text,raw)
- 识别结束(停止或文件识别完成)时触发最终结果。
- Error(operation,message)
- 发生错误时触发。operation 为出错的操作名(如
LoadDefaultModel、OpenModelZip),message 为错误描述(下载失败会注明「首次使用需要联网下载模型」)。
方法
- LoadDefaultModel()
- 加载默认模型(异步)。本地已有缓存就直接加载(秒开);没有就从
DefaultModelUrl联网下载 zip 并解压缓存到应用目录(下载百分比/解压进度经ModelProgress广播,只需这一次),之后全程离线。成功触发ModelLoaded,失败触发Error。 - LoadBundledModel()
- 兼容旧工程的别名,效果与
LoadDefaultModel完全相同(v2.0 曾把模型内置进拓展包,因体积过大导致无法编译 APK,v3.0 起改为首次下载缓存)。新工程请直接用LoadDefaultModel。 - OpenModelZip(zipPath)
- 加载 Vosk 模型压缩包(异步):自动解压到应用目录(已解压过则直接复用、秒开),解压完成自动加载。zipPath 支持三种写法——Web 组件下载后的文件名、
GotFile事件给的绝对路径、或应用目录里的文件名。配合Web.SaveResponse下载大模型,见案例④。 - OpenModel(modelPath)
- 加载设备上已解压的模型目录(异步),目录里须有
am、conf、graph等文件夹(如 adb push 预先解压好的目录)。一般直接用OpenModelZip更省事。 - StartListening()
- 开始实时麦克风识别(需录音权限,需先加载模型)。识别过程中持续触发
PartialResult/Result事件。 - StopListening()
- 停止麦克风识别(会触发一次
FinalResult事件)。 - Pause(paused)
- 暂停或恢复麦克风识别(不打断识别会话)。
- Cancel()
- 取消识别(丢弃当前结果,不触发
FinalResult)。 - RecognizeFile(wavPath)
- 识别一个 WAV 音频文件(16kHz、单声道、16bit PCM)。wavPath 支持文件名(先查应用目录再查工程素材,见案例③)或绝对路径。结果通过
Result/FinalResult事件返回。
性能、内存与体积
- APK 体积:导入本拓展约增加 6MB(引擎与原生库);模型不占 APK 体积(首次运行时下载缓存,默认中文模型 zip 约 42MB、解压后约 68MB)。
- 首次流量:默认中文模型约 42MB,只在第一次发生;之后零流量、全程离线。
- 运行内存:小模型约 300~500MB,1.3GB 大模型需 2GB 以上空闲内存。低端机建议只装小模型,并在长时间不用时换回小模型或重启应用。
- 首次下载+解压:视网速与机型,通常十几秒到一分钟,进度经
ModelProgress广播;之后直接复用缓存目录。 - 识别延迟:小模型在主流手机上
PartialResult约 200~500ms 出一次,可用作实时字幕;大模型更准但出字更慢。
排错表
| 现象 | 原因 | 处理 |
|---|---|---|
| 「加载内置模型失败:当前扩展未内置模型」 | 用的是 v1.0 旧拓展 | 删除旧拓展,重新导入本页下载的 v3.0 aix |
| 「下载或加载默认模型失败:… HTTP xxx」 | 首次使用时没联网,或默认地址不通 | 连网后重试;仍不行就把 DefaultModelUrl 换成 https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip |
| 「下载或加载默认模型失败:… 保存下载文件失败」 | 存储空间不足(需约 110MB:zip + 解压) | 清理存储后重试 |
| 点开始没反应,无任何事件 | 录音权限被拒 | 系统设置里给应用授予麦克风权限;Error 事件里也会提示 |
ModelLoaded 一直不来 |
正在首次下载/解压(42MB) | 看 ModelProgress 事件文案(有下载百分比);存储不足或断网会走 Error |
| 提示「找不到模型压缩包」 | OpenModelZip 的文件名写错,或下载没完成 |
先下载到应用目录(路线 A/B 更省事);Web 路线等 GotFile 里的 fileName 再传给 OpenModelZip |
| 提示「压缩包里没有找到 Vosk 模型」 | zip 不是 Vosk 模型(比如下错了源码包) | 从 alphacephei.com/vosk/models 重新下载,zip 名以 vosk-model 开头 |
OpenModel 提示”请指向解压后的模型目录” |
传的是 zip 路径或目录层级不对 | 直接改用 OpenModelZip;或确认目录内有 am/conf/graph |
| 识别结果是乱码/完全不对 | 采样率不匹配或模型语言不对 | SampleRate 保持 16000;中文用 cn 模型、英文用 en 模型 |
RecognizeFile 报「找不到音频文件」 |
文件名写错/没上传为素材 | 素材面板确认文件名(区分大小写);或放到应用目录用文件名引用 |
RecognizeFile 转出来是空的 |
wav 不是 16kHz 单声道 16bit PCM | 用 Audacity/格式工厂转换参数后重试 |
| 伴侣下能用、打包 APK 后找不到模型 | 两边数据目录不同,绝对路径写死了 | 用文件名(相对路径)引用,不要写死 /sdcard/... 绝对路径 |
| 下载 1.3GB 模型中途失败 | 网络波动/存储不足 | 走 Web 路线可控下载,或用电脑下载后 adb push;先确认剩余空间 > 模型解压后体积(约 2.4GB) |
| 大模型加载后闪退 | 运行内存不足 | 换回小模型;或只在需要时加载、用完不再常驻 |
| 讲完一句后字幕不动了 | 正常:Result 按「句」触发,安静期间不产生新结果 |
继续说话会触发下一句的 Result;逐句处理逻辑写在 Result 事件里(案例② 就是这么做的),不需要反复 Start/Stop |
延伸阅读
- 配合
TextToSpeech(文本转语音):识别 → 处理 → 播报,离线语音助手。 - 配合
LLM大模型拓展:Result的文本直接当提问发给大模型,做离线唤醒 + 在线问答。 - 案例④ 的「改地址换模型 + 有缓存秒开」结构同样适用于其它大资源(词库、地图包、字体包)。
扫码添加客服咨询