FunASR Android客户端开发:移动端语音识别SDK集成
在移动应用开发中,实时语音识别功能往往面临三大挑战:**响应延迟高**、**离线部署困难**、**资源占用大**。传统方案要么依赖云端API导致隐私泄露风险,要么本地部署模型体积过大影响用户体验。FunASR作为达摩院开源的端到端语音识别工具包,通过**WebSocket客户端-服务端架构**,在保证识别精度的同时,实现了移动端轻量化集成。本文将以Android平台为例,详解如何基于FunASR
FunASR Android客户端开发:移动端语音识别SDK集成
引言:移动端语音识别的痛点与解决方案
在移动应用开发中,实时语音识别功能往往面临三大挑战:响应延迟高、离线部署困难、资源占用大。传统方案要么依赖云端API导致隐私泄露风险,要么本地部署模型体积过大影响用户体验。FunASR作为达摩院开源的端到端语音识别工具包,通过WebSocket客户端-服务端架构,在保证识别精度的同时,实现了移动端轻量化集成。本文将以Android平台为例,详解如何基于FunASR SDK构建实时语音识别功能,涵盖服务端部署、客户端开发、通信协议解析全流程。
读完本文你将掌握:
- 快速搭建FunASR实时语音服务
- Android客户端WebSocket通信实现
- 音频录制与流式传输优化
- 热词定制与识别结果处理
- 常见问题排查与性能调优
技术架构概览
FunASR Android客户端采用C/S架构,通过WebSocket协议与服务端进行实时交互。系统整体架构如下:
核心技术栈:
- 服务端:Docker容器化部署,集成VAD/Paraformer-ASR/PUNC模型
- 客户端:OkHttp网络库、AudioRecord音频采集、自定义View波形显示
- 通信协议:JSON格式配置消息 + PCM音频流传输
- 安全层:SSL证书验证(支持自签名证书配置)
服务端环境搭建
Docker快速部署
FunASR提供预构建的Docker镜像,支持一键启动实时语音服务:
# 1. 安装Docker(如未安装)
curl -O https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/shell/install_docker.sh
sudo bash install_docker.sh
# 2. 拉取并启动镜像
sudo docker pull registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.10
mkdir -p ./funasr-runtime-resources/models
sudo docker run -p 10096:10095 -it --privileged=true \
-v $PWD/funasr-runtime-resources/models:/workspace/models \
registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.10
启动2pass识别服务
容器启动后,执行以下命令启动支持实时+非实时协同的2pass服务:
cd FunASR/runtime
nohup bash run_server_2pass.sh \
--download-model-dir /workspace/models \
--vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx \
--model-dir damo/speech_paraformer-large-vad-punc_asr_nat-zh-cn-16k-common-vocab8404-onnx \
--online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx \
--punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx \
--hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
关键参数说明:
| 参数 | 说明 | 默认值 |
|---|---|---|
| --vad-dir | VAD模型路径 | damo/speech_fsmn_vad_zh-cn-16k-common-onnx |
| --model-dir | 非实时ASR模型路径 | speech_paraformer-large-vad-punc_asr_nat... |
| --online-model-dir | 流式ASR模型路径 | speech_paraformer-large_asr_nat... |
| --port | 服务端口 | 10095 |
| --hotword | 全局热词文件路径 | 无 |
| --certfile | SSL证书路径(设为0关闭SSL) | ../../../ssl_key/server.crt |
Android客户端开发实战
开发环境配置
项目依赖
在app/build.gradle中添加必要依赖:
dependencies {
// 网络通信
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
// 基础UI组件
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
}
权限配置
在AndroidManifest.xml中声明必要权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
核心功能实现
1. WebSocket通信模块
使用OkHttp库实现WebSocket客户端,处理SSL证书验证:
// SSLSocketClient.java
public class SSLSocketClient {
public static SSLSocketFactory getSSLSocketFactory() {
try {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, getTrustManager(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 信任所有证书(测试环境用)
private static TrustManager[] getTrustManager() {
return new TrustManager[]{new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {}
@Override
public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; }
}};
}
// 绕过域名验证
public static HostnameVerifier getHostnameVerifier() {
return (s, sslSession) -> true;
}
}
2. 音频录制与传输
在MainActivity中实现音频采集与WebSocket发送逻辑:
// 初始化音频录制
private void initAudioRecord() {
int bufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE, CHANNEL_IN_MONO, ENCODING_PCM_16BIT);
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
SAMPLE_RATE,
CHANNEL_IN_MONO,
ENCODING_PCM_16BIT,
bufferSize);
}
// 开始录制并发送
private void startRecording() {
audioRecord.startRecording();
new Thread(() -> {
byte[] buffer = new byte[SEND_SIZE];
while (isRecording) {
int readSize = audioRecord.read(buffer, 0, SEND_SIZE);
if (readSize > 0) {
// 发送PCM音频数据
webSocket.send(ByteString.of(buffer, 0, readSize));
// 更新波形显示
audioView.post(() -> audioView.setWaveData(buffer));
}
}
}).start();
}
3. WebSocket消息处理
实现WebSocket连接、配置消息发送、识别结果接收完整流程:
private void connectWebSocket() {
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(SSLSocketClient.getSSLSocketFactory(),
SSLSocketClient.getX509TrustManager())
.hostnameVerifier(SSLSocketClient.getHostnameVerifier())
.build();
Request request = new Request.Builder().url(ASR_HOST).build();
webSocket = client.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
// 连接成功后发送配置消息
String configMsg = buildConfigMessage();
webSocket.send(configMsg);
}
@Override
public void onMessage(WebSocket webSocket, String text) {
// 处理服务端返回的JSON结果
handleRecognitionResult(text);
}
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
runOnUiThread(() ->
Toast.makeText(MainActivity.this, "连接失败: " + t.getMessage(),
Toast.LENGTH_SHORT).show());
}
});
}
// 构建配置消息
private String buildConfigMessage() {
try {
JSONObject config = new JSONObject();
config.put("mode", "2pass");
config.put("wav_name", "android_client");
config.put("wav_format", "pcm");
config.put("is_speaking", true);
config.put("chunk_size", new JSONArray("[5,10,5]"));
// 添加热词配置
if (!hotWords.isEmpty()) {
config.put("hotwords", buildHotWordsJson());
}
return config.toString();
} catch (JSONException e) {
e.printStackTrace();
return "";
}
}
UI界面设计
布局文件(activity_main.xml)
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 识别结果显示 -->
<TextView
android:id="@+id/result_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/record_button"
android:textSize="18sp"
android:padding="16dp"/>
<!-- 波形显示控件 -->
<com.yeyupiaoling.androidclient.AudioView
android:id="@+id/audioView"
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_above="@id/record_button"
android:visibility="gone"/>
<!-- 录音按钮 -->
<Button
android:id="@+id/record_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="按下录音"
android:layout_margin="10dp"/>
</RelativeLayout>
录音按钮交互逻辑
recordBtn.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// 按下开始录音
isRecording = true;
startRecording();
recordBtn.setText("录音中...");
audioView.setVisibility(View.VISIBLE);
} else if (event.getAction() == MotionEvent.ACTION_UP) {
// 松开停止录音
isRecording = false;
stopRecording();
recordBtn.setText("按下录音");
audioView.setVisibility(View.GONE);
// 发送结束标志
webSocket.send(new JSONObject().put("is_speaking", false).toString());
}
return true;
});
高级功能实现
热词管理
实现热词输入与持久化存储:
// 显示热词输入对话框
private void showHotWordsDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("设置热词");
EditText input = new EditText(this);
input.setText(loadHotWords()); // 从SharedPreferences加载
builder.setView(input);
builder.setPositiveButton("确定", (dialog, which) -> {
String hotWords = input.getText().toString();
saveHotWords(hotWords); // 保存到SharedPreferences
this.hotWords = hotWords;
});
builder.show();
}
// 构建热词JSON
private String buildHotWordsJson() {
JSONObject hotWordsJson = new JSONObject();
String[] lines = hotWords.split("\n");
for (String line : lines) {
if (line.trim().isEmpty()) continue;
String[] parts = line.split(" ");
if (parts.length == 2) {
try {
hotWordsJson.put(parts[0], Integer.parseInt(parts[1]));
} catch (JSONException e) {
e.printStackTrace();
}
}
}
return hotWordsJson.toString();
}
WebSocket地址配置
允许用户自定义服务端地址:
private void showServerConfigDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("配置服务地址");
EditText input = new EditText(this);
input.setText(sharedPreferences.getString("server_url", DEFAULT_HOST));
builder.setView(input);
builder.setPositiveButton("确定", (dialog, which) -> {
String url = input.getText().toString();
sharedPreferences.edit().putString("server_url", url).apply();
ASR_HOST = url;
});
builder.setNeutralButton("使用默认", (dialog, which) -> {
ASR_HOST = DEFAULT_HOST;
sharedPreferences.edit().putString("server_url", DEFAULT_HOST).apply();
input.setText(DEFAULT_HOST);
});
builder.show();
}
通信协议详解
消息格式定义
FunASR客户端与服务端通过JSON配置消息和二进制音频数据两种类型的消息进行通信。
客户端发送的消息
- 配置消息(首次连接时发送):
{
"mode": "2pass",
"wav_name": "android_client",
"wav_format": "pcm",
"is_speaking": true,
"chunk_size": [5, 10, 5],
"hotwords": "{\"阿里巴巴\":20,\"达摩院\":30}"
}
-
音频数据:
直接发送PCM格式的二进制数据,采样率16kHz,16位单声道。 -
结束标志(录音停止时发送):
{"is_speaking": false}
服务端返回的消息
- 实时识别结果:
{
"mode": "2pass-online",
"text": "这是实时识别结果",
"is_final": false
}
- 最终识别结果(带标点):
{
"mode": "2pass-offline",
"text": "这是经过2pass纠错的最终识别结果。",
"is_final": true,
"timestamp": "[[100, 300], [300, 500]]"
}
状态流转图
测试与部署
客户端测试
- 本地调试:
在Android Studio中直接运行AndroidClient模块,通过Logcat查看详细日志:
adb logcat | grep "WebSocket" # 过滤WebSocket相关日志
adb logcat | grep "ASR" # 过滤识别结果相关日志
- 功能验证:
- 验证录音权限申请流程
- 测试WebSocket地址配置功能
- 验证热词生效(如设置"阿里巴巴 20"后测试识别效果)
- 测试弱网环境下的重连机制
服务端监控
通过Docker日志监控服务状态:
# 查看容器ID
docker ps
# 查看服务日志
docker logs -f [container_id]
# 查看服务端推理性能
tail -f nohup.out | grep "RTF" # 实时查看实时率
性能指标
在中高端Android设备(如骁龙865)上的典型性能:
- 首次连接建立时间:<500ms
- 音频流传输延迟:~80ms
- 实时识别响应:<300ms
- 2pass最终结果延迟:<1s(短句)
- 内存占用:<80MB(应用整体)
- CPU占用:录音时~20%,静默时<5%
常见问题解决
连接类问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 服务端未启动或端口不通 | 检查服务端是否正常运行,验证防火墙配置 |
| SSL握手失败 | 证书无效或域名不匹配 | 服务端关闭SSL(--certfile 0)或客户端禁用证书验证 |
| 配置消息发送失败 | JSON格式错误 | 检查热词格式是否正确,使用try-catch捕获JSONException |
识别类问题
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无识别结果返回 | 音频格式错误 | 确认PCM参数(16kHz/16bit/单声道) |
| 识别准确率低 | 环境噪音大或热词未生效 | 开启降噪处理,检查热词格式是否为"热词 权重" |
| 实时性差 | 网络延迟高 | 优化网络环境,考虑边缘节点部署服务端 |
客户端崩溃
- 权限问题:
确保在Android 6.0+设备上动态申请录音权限:
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.RECORD_AUDIO}, 1);
}
- 内存泄漏:
在onDestroy中释放资源:
@Override
protected void onDestroy() {
super.onDestroy();
if (webSocket != null) {
webSocket.close(1000, "Activity destroyed");
}
if (audioRecord != null) {
audioRecord.release();
}
}
总结与扩展
本文详细介绍了基于FunASR构建Android实时语音识别客户端的全过程,包括服务端部署、客户端开发、通信协议解析和问题排查。通过WebSocket协议实现的客户端-服务端架构,既避免了本地部署大模型的资源限制,又能提供低延迟的实时识别体验。
进阶扩展方向
-
多语言支持:
修改服务端启动参数,加载多语言模型,客户端添加语言选择界面。 -
离线模式:
集成TFLite轻量级模型,实现完全本地识别(参考FunASR Lite项目)。 -
语音唤醒:
结合FunASR的VAD模型,实现关键词唤醒+语音识别联动。 -
自定义UI:
扩展AudioView实现更丰富的音频可视化效果,如频谱图显示。
项目资源
- 客户端源码:
runtime/android/AndroidClient - 服务端部署文档:
runtime/docs/SDK_advanced_guide_online_zh.md - WebSocket协议详情:
runtime/docs/websocket_protocol_zh.md - 模型下载:通过ModelScope下载最新ASR/VAD/PUNC模型
更多推荐
所有评论(0)