OfflineSpeech 拓展:离线语音识别

« 返回首页

logo 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 拓展下载:

cn.fun123.OfflineSpeech.aix


快速上手:3 分钟做一个实时字幕器(案例①)

成果:点「开始说话」对着手机讲话,屏幕上实时滚出文字;点「停止」结束。全程不联网。

器材:一台安卓手机(联网只需两次:第一次从本站下载拓展、第一次启动时下载模型约 42MB)、AI 伴侣或打包 APK 均可。

  1. 导入拓展:设计视图 → 扩展 → 导入拓展 → 从本页下载的 aix 导入。
  2. 搭界面:从组件面板拖入——
    • 标签 ×2:改名 标签_状态(提示语,字号 14)、标签_字幕(字幕,字号 20);
    • 按钮 ×2:改名 按钮_开始(文本「🎤 开始说话」)、按钮_停止(文本「⏹ 停止」);
    • 非可视组件 OfflineSpeech1(扩展分类里)。
  3. 连代码块:
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 = "已停止"
}
  1. 跑起来看现象:
    • 首次启动:标签_状态 依次显示「首次使用:正在下载中文模型(约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 分钟)。

  1. 设计时把 test16k.wav 上传为工程素材(素材面板 → 上传文件);
  2. 搭 标签_全文(多行、高度充填)+ 代码块:
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
}

分步看现象:

  1. 第一次启动:状态栏依次「正在下载中文模型…」(文案是通用提示)→「模型下载中 N%…」→「正在解压默认模型…」→「英文模型就绪」。此时讲 “hello world” 试试。
  2. 第二次启动:直接「英文模型就绪」——缓存已在本地,秒开、零流量。
  3. 断网重启:第 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 的文本直接当提问发给大模型,做离线唤醒 + 在线问答。
  • 案例④ 的「改地址换模型 + 有缓存秒开」结构同样适用于其它大资源(词库、地图包、字体包)。
文档反馈