【实战】Android端侧会议纪要语音识别方案全解析(离线ASR+说话人分离+标点)

一、前言

在移动办公场景下,离线会议纪要功能(语音转文字+说话人分离+标点恢复)成为刚需,但Android端侧实现面临模型适配、内存优化、离线运行等多重挑战。本文基于实际调研,对比主流开源/商业方案,提供可落地的端侧语音识别全流程解决方案,覆盖技术选型、代码实现、性能优化等核心环节。

二、核心需求与技术挑战

2.1 核心需求(P0级别)

功能模块 具体要求 性能指标
语音转文字(ASR) 中英离线识别 CER<10%(准确率>90%)
说话人分离 区分不同说话人 DER<20%
标点恢复 自动添加标点 可读性提升80%+
离线运行 无网络依赖 Android 8.0+适配
资源限制 内存/模型体积 内存<2GB,总模型<1.5GB

2.2 关键技术挑战

  1. 模块集成难:ASR、说话人分离、标点多为独立模块,缺少端到端Pipeline;
  2. Android移植成本高:主流方案(Whisper、Pyannote)基于Python,需转ONNX/TFLite;
  3. 内存优化压力大:大模型易触发Low Memory Killer,轻量级选型/量化是关键;
  4. 实时性与准确率平衡:离线场景需兼顾处理速度与识别效果。

三、主流方案对比(选型核心)

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移植关键步骤
  1. 模型转换(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
  1. 基于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 内存优化(核心!避免崩溃)

  1. 模型量化:INT8量化减少50%内存占用(优先选.int8.onnx模型);
  2. 分步释放:处理完说话人分离立即释放该模型,再加载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
}
  1. 小模型选型:用Paraformer-small(80MB)替代large(220MB),Whisper-base替代large。

5.2 速度优化

  1. 多线程并行:多个说话人片段并行处理;
val executor = Executors.newFixedThreadPool(4) // 4线程池
val futures = speakerSegments.map { segment ->
    executor.submit { processSingleSegment(segment) }
}
val results = futures.map { it.get() }
executor.shutdown()
  1. NPU加速:Sherpa-ONNX支持高通/瑞芯微NPU,开启后速度提升3-5倍;
  2. 批处理:ASR设置batch_size=16,提升批量处理效率。

5.3 准确率优化

  1. 热词定制:FunASR/Sherpa支持添加行业热词(如“海思”“达芬奇”);
  2. 音频预处理:降噪、音量归一化(可使用Android原生AudioEffect);
  3. 模型微调:基于自有会议数据微调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

  1. Q:4GB RAM设备能否运行?
    A:可以,使用INT8量化的Paraformer-small+Pyannote,总内存<800MB;
  2. Q:是否支持实时转录?
    A:Sherpa-ONNX支持流式ASR,说话人分离需离线(可折中用VAD分段);
  3. Q:多语言支持?
    A:WhisperX支持99种语言,FunASR主打中英日,Picovoice支持20+语言。

本文 Android 端侧离线 ASR 全套代码、模型包、部署文档已同步至我的公众号 【科技低语】
关注回复 【端侧语音】 一键领取,持续更新端侧 AI 落地实战。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐