全网最全手机端实现语音转文字算法全离线运行方案,语音转文字+说话人分离+VAD+长音频识别
·
【实战】Android端侧会议纪要语音识别方案全解析(离线ASR+说话人分离+标点)
一、前言
在移动办公场景下,离线会议纪要功能(语音转文字+说话人分离+标点恢复)成为刚需,但Android端侧实现面临模型适配、内存优化、离线运行等多重挑战。本文基于实际调研,对比主流开源/商业方案,提供可落地的端侧语音识别全流程解决方案,覆盖技术选型、代码实现、性能优化等核心环节。
二、核心需求与技术挑战
2.1 核心需求(P0级别)
| 功能模块 | 具体要求 | 性能指标 |
|---|---|---|
| 语音转文字(ASR) | 中英离线识别 | CER<10%(准确率>90%) |
| 说话人分离 | 区分不同说话人 | DER<20% |
| 标点恢复 | 自动添加标点 | 可读性提升80%+ |
| 离线运行 | 无网络依赖 | Android 8.0+适配 |
| 资源限制 | 内存/模型体积 | 内存<2GB,总模型<1.5GB |
2.2 关键技术挑战
- 模块集成难:ASR、说话人分离、标点多为独立模块,缺少端到端Pipeline;
- Android移植成本高:主流方案(Whisper、Pyannote)基于Python,需转ONNX/TFLite;
- 内存优化压力大:大模型易触发Low Memory Killer,轻量级选型/量化是关键;
- 实时性与准确率平衡:离线场景需兼顾处理速度与识别效果。
三、主流方案对比(选型核心)
3.1 方案总览(按推荐优先级排序)
| 方案 | 集成度 | Android支持 | 开源属性 | 开发成本 | 核心优势 | 适用场景 |
|---|---|---|---|---|---|---|
| Picovoice Leopard+Falcon | 端到端 | 原生SDK | 商业(有免费额度) | 低(1周) | 轻量(<40MB)、离线、适配优 | 有预算、追求快速落地 |
| WhisperX | 端到端 | 需移植(ONNX) | 开源(MIT) | 中(2-4周) | 准确率高(业界标杆)、多语言 | 无预算、追求极致效果 |
| FunASR Pipeline | 端到端 | 需移植(ONNX) | 开源 | 中(2-4周) | 中文优化、方言支持 | 中文场景为主 |
| Sherpa-ONNX手动集成 | 模块化 | 原生SDK | 开源(Apache 2.0) | 高(3-5周) | 完全可控、灵活定制 | 深度定制化需求 |
3.2 技术栈选型建议
| 技术栈 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Python(PC验证) | 生态成熟、调试快 | 无法直接部署Android | 原型验证 |
| ONNX Runtime | 跨平台、性能优 | 需模型转换 | 主流移植方案 |
| TensorFlow Lite | Android原生支持 | 模型覆盖有限 | 轻量简单场景 |
| Native C++ | 性能最优 | 开发成本极高 | 极致性能需求 |
四、落地方案详解(附完整代码)
本文 Android 端侧离线 ASR 全套代码、模型包、部署文档已同步至我的公众号 【科技低语】
关注回复 【端侧语音】 一键领取,持续更新端侧 AI 落地实战。
4.1 方案1:Picovoice(商业首选,最快落地)
4.1.1 核心优势
- 端到端集成:一个SDK覆盖ASR+说话人分离+标点,无需拼接模块;
- 移动端优化:模型<40MB,内存占用<500MB,适配Android 8.0+;
- 完全离线:识别/分离全本地,仅License验证需临时网络。
4.1.2 Android集成代码(可直接复用)
import ai.picovoice.leopard.*;
import ai.picovoice.falcon.*;
import android.content.Context;
public class PicovoiceMeetingTranscriber {
private Leopard leopard;
private Falcon falcon;
private Context context;
// 初始化(建议放在Application中)
public void init(Context ctx) {
this.context = ctx;
try {
// 初始化ASR(带标点)
leopard = new Leopard.Builder()
.setAccessKey("你的AccessKey") // 官网申请免费额度
.build(context);
// 初始化说话人分离
falcon = new Falcon.Builder()
.setAccessKey("你的AccessKey")
.build(context);
} catch (Exception e) {
e.printStackTrace();
}
}
// 核心转录方法
public void transcribe(String audioPath) {
try {
// 1. ASR识别(带标点)
LeopardTranscript transcript = leopard.processFile(audioPath);
// 2. 说话人分离
FalconSegment[] segments = falcon.processFile(audioPath);
// 3. 组合结果(按时间对齐)
for (FalconSegment segment : segments) {
String speakerText = extractTextByTime(transcript, segment.startSec, segment.endSec);
System.out.printf("[说话人%s] %.2f-%.2f: %s\n",
segment.speakerTag, segment.startSec, segment.endSec, speakerText);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 按时间截取文本(辅助方法)
private String extractTextByTime(LeopardTranscript transcript, float start, float end) {
StringBuilder sb = new StringBuilder();
for (LeopardWord word : transcript.getWords()) {
if (word.getStartSec() >= start && word.getEndSec() <= end) {
sb.append(word.getWord()).append(" ");
}
}
return sb.toString().trim();
}
// 释放资源(必须调用)
public void release() {
if (leopard != null) leopard.delete();
if (falcon != null) falcon.delete();
}
}
4.1.3 成本说明
- 免费额度:每月60小时转录时长(满足中小团队测试/轻度使用);
- 付费版:≈$0.015/分钟,企业私有部署需商务洽谈。
4.2 方案2:WhisperX(开源标杆,高准确率)
4.2.1 技术架构
音频输入 → Whisper Large V3(ASR+标点)→ Wav2Vec2(词级对齐)→ Pyannote(说话人分离)→ 结果输出
4.2.2 Python原型验证(快速验证效果)
import whisperx
import torch
# 配置(CPU模式,适配移动端)
device = "cpu"
compute_type = "int8" # 量化减小内存
# 1. 加载音频
audio = whisperx.load_audio("meeting.wav")
# 2. ASR识别(带标点)
model = whisperx.load_model("large-v3", device, compute_type=compute_type)
result = model.transcribe(audio, batch_size=16)
# 3. 词级时间对齐
align_model, metadata = whisperx.load_align_model(language_code="zh", device=device)
result = whisperx.align(result["segments"], align_model, metadata, audio, device=device)
# 4. 说话人分离
diarize_model = whisperx.DiarizationPipeline(device=device)
diarize_segments = diarize_model(audio)
result = whisperx.assign_word_speakers(diarize_segments, result)
# 5. 输出结果
for segment in result["segments"]:
print(f"[说话人{segment['speaker']}] {segment['start']:.2f}-{segment['end']:.2f}: {segment['text']}")
4.2.3 Android移植关键步骤
- 模型转换(ONNX量化)
# 导出Whisper为ONNX
python export_whisper_onnx.py --model large-v3 --output whisper.onnx
# INT8量化(减小50%体积)
python -m onnxruntime.quantization.preprocess --input whisper.onnx --output whisper_int8.onnx
- 基于Sherpa-ONNX集成:复用Sherpa的Android SDK,替换Pyannote为Sherpa的说话人分离模块。
4.3 方案3:Sherpa-ONNX(完全开源,深度定制)
4.3.1 核心优势
- Android原生支持:提供Kotlin/Java API,无需手动移植;
- 模块化设计:可自由组合ASR(Paraformer/Whisper)、说话人分离、标点模块;
- 轻量化:INT8量化后总内存<1GB,适配4GB+设备。
4.3.2 完整Android实现(可直接运行)
package com.meeting.asr
import android.content.Context
import android.content.res.AssetManager
import com.k2fsa.sherpa.onnx.*
import java.io.File
// 结果数据类
data class TranscriptionResult(
val speaker: Int, // 说话人编号
val startTime: Float, // 开始时间(秒)
val endTime: Float, // 结束时间(秒)
val text: String // 识别文本(带标点)
)
/**
* 端侧会议转录Pipeline(Sherpa-ONNX)
* 集成:说话人分离 + ASR + 标点恢复
*/
class SherpaMeetingPipeline(private val context: Context) {
private val assetManager: AssetManager = context.assets
private var diarization: OfflineSpeakerDiarization? = null
private var asr: OfflineRecognizer? = null
private var punctuation: OfflinePunctuation? = null
// 初始化模型(建议异步执行)
fun initModels() {
// 1. 初始化说话人分离
val diarizationConfig = OfflineSpeakerDiarizationConfig(
segmentation = OfflineSpeakerSegmentationModelConfig(
pyannote = OfflineSpeakerSegmentationPyannoteModelConfig(
model = "pyannote-segmentation-3-0/model.int8.onnx" // 量化模型
),
numThreads = 4 // 线程数(根据设备调整)
),
embedding = SpeakerEmbeddingExtractorConfig(
model = "3dspeaker_eres2net_base_sv_zh-cn_16k.int8.onnx",
numThreads = 2
),
clustering = FastClusteringConfig(
numClusters = -1, // 自动检测说话人数量
threshold = 0.5f // 聚类阈值(可调)
),
minDurationOn = 0.2f, // 最小语音段
minDurationOff = 0.5f // 最小静音段
)
diarization = OfflineSpeakerDiarization(assetManager, diarizationConfig)
// 2. 初始化ASR(Paraformer中文模型)
val asrConfig = OfflineRecognizerConfig(
modelConfig = OfflineModelConfig(
paraformer = OfflineParaformerModelConfig(
model = "paraformer-zh/model.int8.onnx"
),
tokens = "paraformer-zh/tokens.txt",
numThreads = 4,
debug = false
)
)
asr = OfflineRecognizer(assetManager, asrConfig)
// 3. 初始化标点恢复
val puncConfig = OfflinePunctuationConfig(
model = OfflinePunctuationModelConfig(
ctTransformer = "ct-punc/model.int8.onnx",
numThreads = 2
)
)
punctuation = OfflinePunctuation(assetManager, puncConfig)
}
/**
* 核心转录方法
* @param audioPath 音频文件路径(WAV,16kHz)
* @param progressCallback 进度回调
* @return 转录结果列表
*/
fun transcribe(
audioPath: String,
progressCallback: (Float) -> Unit = {}
): List<TranscriptionResult> {
val results = mutableListOf<TranscriptionResult>()
try {
// 1. 读取音频
val waveData = getWaveData(audioPath)
val samples = waveData.samples
val sampleRate = waveData.sampleRate
check(sampleRate == 16000) { "仅支持16kHz采样率" }
// 2. 说话人分离(带进度)
val speakerSegments = diarization?.processWithCallback(samples) { processed, total, _ ->
val progress = processed * 100.0f / total
progressCallback(progress)
0 // 继续处理
} ?: emptyList()
// 3. 逐段ASR+标点
speakerSegments.forEachIndexed { index, segment ->
progressCallback(50 + index * 50f / speakerSegments.size)
// 截取当前说话人音频片段
val startSample = (segment.start * sampleRate).toInt()
val endSample = (segment.end * sampleRate).toInt()
val segmentSamples = samples.copyOfRange(startSample, endSample)
// ASR识别
val stream = asr?.createStream()
stream?.acceptWaveform(segmentSamples, sampleRate)
val asrResult = asr?.decode(stream) ?: ""
stream?.release()
// 添加标点
val textWithPunc = if (asrResult.isNotEmpty()) {
punctuation?.addPunctuation(asrResult) ?: asrResult
} else {
""
}
// 保存结果
if (textWithPunc.isNotEmpty()) {
results.add(
TranscriptionResult(
speaker = segment.speaker,
startTime = segment.start,
endTime = segment.end,
text = textWithPunc
)
)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return results
}
// 释放资源(页面销毁时调用)
fun release() {
diarization?.release()
asr?.release()
punctuation?.release()
diarization = null
asr = null
punctuation = null
}
// 读取WAV文件(Sherpa-ONNX工具方法)
private fun getWaveData(path: String): WaveData {
val file = File(path)
check(file.exists()) { "音频文件不存在:$path" }
return WaveReader.read(file.absolutePath)
}
}
// 使用示例
fun useExample(context: Context) {
val pipeline = SherpaMeetingPipeline(context)
// 异步初始化
Thread {
pipeline.initModels()
// 转录音频
val results = pipeline.transcribe("/sdcard/meeting.wav") { progress ->
// 更新UI进度
println("进度:${progress}%")
}
// 输出结果
results.forEach {
println("[说话人${it.speaker}] ${it.startTime}-${it.endTime}s: ${it.text}")
}
// 释放资源
pipeline.release()
}.start()
}
4.3.3 模型准备(一键下载)
# 1. 说话人分割模型
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/sherpa-onnx-pyannote-segmentation-3-0.tar.bz2
# 2. 说话人嵌入模型(中文)
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-recongition-models/3dspeaker_speech_eres2net_base_sv_zh-cn_3dspeaker_16k.onnx
# 3. ASR模型(Paraformer中文)
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-paraformer-zh-2024-03-09.tar.bz2
# 4. 标点模型
wget https://github.com/k2-fsa/sherpa-onnx/releases/download/punctuation-models/sherpa-onnx-punct-ct-transformer-zh-en-vocab272727-2024-04-12.tar.bz2
# 解压后放入Android assets目录
五、性能优化实战
5.1 内存优化(核心!避免崩溃)
- 模型量化:INT8量化减少50%内存占用(优先选.int8.onnx模型);
- 分步释放:处理完说话人分离立即释放该模型,再加载ASR;
// 优化后的释放逻辑
fun transcribeOptimized(audioPath: String): List<TranscriptionResult> {
// 步骤1:说话人分离(用完即释)
val segments = diarization?.process(samples)
diarization?.release()
diarization = null
// 步骤2:ASR+标点(用完即释)
val results = processASR(segments)
asr?.release()
asr = null
// 步骤3:标点处理
val finalResults = addPunctuation(results)
punctuation?.release()
punctuation = null
return finalResults
}
- 小模型选型:用Paraformer-small(80MB)替代large(220MB),Whisper-base替代large。
5.2 速度优化
- 多线程并行:多个说话人片段并行处理;
val executor = Executors.newFixedThreadPool(4) // 4线程池
val futures = speakerSegments.map { segment ->
executor.submit { processSingleSegment(segment) }
}
val results = futures.map { it.get() }
executor.shutdown()
- NPU加速:Sherpa-ONNX支持高通/瑞芯微NPU,开启后速度提升3-5倍;
- 批处理:ASR设置batch_size=16,提升批量处理效率。
5.3 准确率优化
- 热词定制:FunASR/Sherpa支持添加行业热词(如“海思”“达芬奇”);
- 音频预处理:降噪、音量归一化(可使用Android原生AudioEffect);
- 模型微调:基于自有会议数据微调Paraformer/CAM++模型。
六、实施落地指南
6.1 分阶段实施计划
| 阶段 | 周期 | 核心任务 | 交付物 |
|---|---|---|---|
| 原型验证 | 1-2周 | PC端测试WhisperX/FunASR,评估效果 | 验证报告+性能数据 |
| Android集成 | 2-4周 | 模型转换、代码编写、基础测试 | Android SDK+示例App |
| 性能优化 | 1-2周 | 内存/速度优化、兼容性测试 | 优化版SDK+测试报告 |
| 上线维护 | 持续 | 文档编写、用户反馈修复 | 正式版+维护手册 |
6.2 风险应对
| 风险 | 应对措施 |
|---|---|
| 内存不足崩溃 | 量化模型+分步释放+最低配置限制(4GB RAM) |
| 准确率不达标 | 更换大模型+热词+音频预处理 |
| 处理速度慢 | 小模型+NPU加速+并行处理 |
| 设备兼容问题 | 测试主流机型+提供降级方案 |
七、资源汇总(一键收藏)
7.1 核心项目
- Sherpa-ONNX:https://github.com/k2-fsa/sherpa-onnx(Android首选)
- WhisperX:https://github.com/m-bain/whisperX(高准确率)
- FunASR:https://github.com/modelscope/FunASR(中文优化)
- Picovoice:https://picovoice.ai/(商业方案)
7.2 模型下载
- Sherpa-ONNX模型合集:https://github.com/k2-fsa/sherpa-onnx/releases
- FunASR模型:https://www.modelscope.cn/models?tasks=auto-speech-recognition
- Whisper ONNX:https://github.com/k2-fsa/sherpa-onnx/releases/tag/asr-models
7.3 技术文档
- Sherpa-ONNX Android指南:https://k2-fsa.github.io/sherpa/onnx/android/index.html
- FunASR中文文档:https://github.com/modelscope/FunASR/blob/main/README_zh.md
- Picovoice Android示例:https://github.com/Picovoice/leopard-android
八、总结
- 有预算:优先选Picovoice,1周落地,效果稳定;
- 无预算+高准确率:选WhisperX/FunASR,2-4周移植,适配中文场景;
- 深度定制:选Sherpa-ONNX,3-5周开发,完全可控;
- 核心优化点:模型量化+分步释放(解决内存)、多线程+NPU(解决速度)、热词+微调(解决准确率)。
附:常见问题FAQ
- Q:4GB RAM设备能否运行?
A:可以,使用INT8量化的Paraformer-small+Pyannote,总内存<800MB; - Q:是否支持实时转录?
A:Sherpa-ONNX支持流式ASR,说话人分离需离线(可折中用VAD分段); - Q:多语言支持?
A:WhisperX支持99种语言,FunASR主打中英日,Picovoice支持20+语言。
本文 Android 端侧离线 ASR 全套代码、模型包、部署文档已同步至我的公众号 【科技低语】
关注回复 【端侧语音】 一键领取,持续更新端侧 AI 落地实战。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)