欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),
一起共建开源鸿蒙跨平台生态。
一、引言
在移动应用开发中,语音交互(TTS 文本转语音、STT 语音转文本)已成为提升用户体验的核心功能之一。尤其在鸿蒙(HarmonyOS)与 Flutter 混合开发场景下,全离线部署的语音交互能突破网络依赖限制,同时保障用户隐私安全,广泛适用于智能设备控制、离线导航、无障碍辅助等场景。
本文将聚焦 鸿蒙 + Flutter 混合开发架构,从环境搭建、离线引擎选型、TTS/STT 核心实现、多语言适配到性能优化,全方位拆解语音交互的全离线部署方案。文中包含大量可直接复用的代码示例、官方文档链接及实战避坑指南,助力开发者快速落地高质量的离线语音功能。
1.1 核心目标
- 基于鸿蒙原生能力 + Flutter 跨平台特性,实现 TTS/STT 全离线运行(无网络环境下正常工作);
- 支持中英日韩等多语言的语音合成与识别,适配不同地区用户需求;
- 优化离线资源占用、响应速度及识别准确率,平衡功能与性能;
- 提供 Flutter 与鸿蒙原生的无缝通信方案,降低混合开发复杂度。
1.2 技术栈选型
| 技术方向 | 选型方案 | 核心优势 | 官方文档链接 |
|---|---|---|---|
| 跨平台框架 | Flutter 3.13+ | 高性能 UI 渲染、跨平台一致性、丰富的生态 | Flutter 官方文档 |
| 系统底座 | HarmonyOS 4.0+ | 原生离线语音能力、分布式架构支持、权限管控完善 | 鸿蒙开发者文档 |
| 离线 TTS 引擎 | 鸿蒙原生 TTS 引擎 + 离线语音包 | 系统级集成、低延迟、支持多语言 | 鸿蒙 TTS 开发指南 |
| 离线 STT 引擎 | PocketSphinx + 鸿蒙 NDK 编译 | 轻量开源、支持离线识别、可自定义词典 | PocketSphinx 官方仓库 |
| 跨端通信 | Flutter MethodChannel | 高效双向通信、支持复杂数据传递 | Flutter MethodChannel 文档 |
二、环境准备
2.1 开发环境搭建
2.1.1 鸿蒙开发环境
- 安装 DevEco Studio 4.0+(需配置鸿蒙 SDK 4.0+,包含 AI 模块、NDK 工具链);
- 下载地址:DevEco Studio 官网
- 配置鸿蒙模拟器(API 版本 9+)或真实设备(需开启开发者模式);
- 启用鸿蒙原生 AI 能力:在
config.json中添加 AI 权限与模块依赖。
2.1.2 Flutter 开发环境
-
安装 Flutter 3.13+(需兼容鸿蒙系统): bash
运行
# 安装指定版本 Flutter flutter version 3.13.0 # 配置鸿蒙编译环境 flutter config --enable-harmonyos -
创建 Flutter 混合工程: bash
运行
flutter create --platforms=harmonyos my_voice_app cd my_voice_app -
集成鸿蒙原生依赖:在
harmonyos/build.gradle中添加 TTS/STT 相关依赖:groovy
dependencies { // 鸿蒙原生 TTS 依赖 implementation 'com.huawei.hms:ai-tts:4.0.0.300' // 鸿蒙 NDK 支持(用于编译 PocketSphinx) implementation 'com.huawei.hms:ndk-utils:1.0.0.300' }
2.2 离线资源准备
2.2.1 鸿蒙 TTS 离线语音包
- 下载地址:鸿蒙离线语音包官网
- 支持语言:中文(普通话)、英文、日语、韩语等(需下载对应语言的
.hap格式资源包); - 部署方式:将语音包放入工程
harmonyos/src/main/raw目录,或通过应用内下载后解压至沙箱路径。
2.2.2 PocketSphinx 离线模型
- 下载地址:PocketSphinx 预训练模型
- 核心文件:
- 语言模型:
en-us.lm.bin(英文)、zh-CN.lm.bin(中文); - 声学模型:
hub4wsj_sc_8k.tar.gz(英文)、zh_cn.cd_cont_5000.tar.gz(中文); - 字典文件:
cmudict-en-us.dict(英文)、mandarin_notone.dic(中文);
- 语言模型:
- 部署方式:将模型文件放入
harmonyos/src/main/assets目录,离线运行时加载。
三、核心实现:Flutter 与鸿蒙原生通信
鸿蒙 + Flutter 混合开发的核心是 跨端通信,通过 Flutter MethodChannel 实现 Dart 代码与鸿蒙 Java 代码的双向调用。以下是通用通信架构设计:
3.1 通信架构设计
MethodCall
转发调用
调用 TTS/STT 引擎
返回结果
MethodResult
回调结果
Flutter 端(Dart)
MethodChannel
鸿蒙原生端(Java)
离线引擎(鸿蒙 TTS/PocketSphinx)
3.2 通用通信封装(Flutter 端)
创建 lib/voice_channel.dart,封装 TTS/STT 相关的 MethodChannel 调用:
dart
import 'package:flutter/services.dart';
class VoiceChannel {
// 定义 MethodChannel 名称(需与鸿蒙端一致)
static const MethodChannel _channel = MethodChannel('com.example.voice/tts_stt');
// 初始化离线引擎(加载语音包/模型)
static Future<bool> initOfflineEngine({required String language}) async {
try {
final bool result = await _channel.invokeMethod(
'initOfflineEngine',
{'language': language}, // 传递语言参数(如 "zh-CN", "en-US")
);
return result;
} on PlatformException catch (e) {
print('初始化离线引擎失败:${e.message}');
return false;
}
}
// TTS:文本转语音(离线)
static Future<bool> startTts({
required String text,
required double volume, // 音量(0.0-1.0)
required double speed, // 语速(0.5-2.0)
}) async {
try {
final bool result = await _channel.invokeMethod(
'startTts',
{
'text': text,
'volume': volume,
'speed': speed,
},
);
return result;
} on PlatformException catch (e) {
print('TTS 合成失败:${e.message}');
return false;
}
}
// STT:语音转文本(离线)
static Future<String?> startStt() async {
try {
final String? result = await _channel.invokeMethod('startStt');
return result;
} on PlatformException catch (e) {
print('STT 识别失败:${e.message}');
return null;
}
}
// 停止语音播放/识别
static Future<bool> stopVoice() async {
try {
final bool result = await _channel.invokeMethod('stopVoice');
return result;
} on PlatformException catch (e) {
print('停止语音操作失败:${e.message}');
return false;
}
}
}
3.3 通用通信封装(鸿蒙端)
创建 harmonyos/src/main/java/com/example/voice/TtsSttChannel.java,实现 MethodChannel 接收与处理逻辑:
java
运行
import ohos.aafwk.ability.Ability;
import ohos.agp.window.service.WindowManager;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
public class TtsSttChannel implements MethodCallHandler {
private static final HiLogLabel LABEL = new HiLogLabel(HiLog.DEBUG, 0x00100, "TtsSttChannel");
private final Ability ability;
private OfflineTtsManager ttsManager;
private OfflineSttManager sttManager;
// 构造函数(传入当前 Ability)
public TtsSttChannel(Ability ability) {
this.ability = ability;
}
// 注册 MethodChannel
public void registerWith(FlutterEngine flutterEngine) {
new MethodChannel(
flutterEngine.getDartExecutor().getBinaryMessenger(),
"com.example.voice/tts_stt" // 与 Flutter 端一致
).setMethodCallHandler(this);
}
@Override
public void onMethodCall(MethodCall call, Result result) {
switch (call.method) {
case "initOfflineEngine":
// 初始化离线引擎
String language = call.argument("language");
boolean initSuccess = initEngine(language);
result.success(initSuccess);
break;
case "startTts":
// 处理 TTS 调用
String text = call.argument("text");
double volume = call.argument("volume");
double speed = call.argument("speed");
boolean ttsSuccess = startTts(text, volume, speed);
result.success(ttsSuccess);
break;
case "startStt":
// 处理 STT 调用
String sttResult = startStt();
result.success(sttResult);
break;
case "stopVoice":
// 停止语音操作
boolean stopSuccess = stopVoice();
result.success(stopSuccess);
break;
default:
result.notImplemented();
break;
}
}
// 初始化 TTS/STT 离线引擎(后续实现)
private boolean initEngine(String language) {
// TODO: 加载离线语音包与模型
ttsManager = new OfflineTtsManager(ability, language);
sttManager = new OfflineSttManager(ability, language);
return ttsManager.init() && sttManager.init();
}
// 启动 TTS 合成(后续实现)
private boolean startTts(String text, double volume, double speed) {
return ttsManager.speak(text, volume, speed);
}
// 启动 STT 识别(后续实现)
private String startStt() {
return sttManager.recognize();
}
// 停止语音操作(后续实现)
private boolean stopVoice() {
ttsManager.stop();
sttManager.stop();
return true;
}
}
3.4 注册通信通道
在鸿蒙端 MainAbility.java 中注册 MethodChannel:
java
运行
import io.flutter.embedding.harmonyos.FlutterAbility;
import io.flutter.embedding.engine.FlutterEngine;
public class MainAbility extends FlutterAbility {
@Override
public void configureFlutterEngine(FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
// 注册 TTS/STT 通信通道
new TtsSttChannel(this).registerWith(flutterEngine);
}
}
四、离线 TTS 实现(鸿蒙原生 + Flutter 调用)
4.1 鸿蒙原生离线 TTS 封装
创建 harmonyos/src/main/java/com/example/voice/OfflineTtsManager.java,基于鸿蒙原生 TTS 引擎实现离线合成:
java
运行
import ohos.aafwk.ability.Ability;
import ohos.agp.utils.Color;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.audio.AudioManager;
import ohos.media.tts.TtsEngine;
import ohos.media.tts.TtsHelper;
import ohos.media.tts.TtsPlayer;
import ohos.media.tts.TtsRequest;
import ohos.media.tts.TtsResponse;
import ohos.media.tts.TtsSpeaker;
import java.util.List;
public class OfflineTtsManager {
private static final HiLogLabel LABEL = new HiLogLabel(HiLog.DEBUG, 0x00101, "OfflineTtsManager");
private final Ability ability;
private final String language;
private TtsHelper ttsHelper;
private TtsPlayer ttsPlayer;
private boolean isInitialized = false;
// 构造函数(传入 Ability 与目标语言)
public OfflineTtsManager(Ability ability, String language) {
this.ability = ability;
this.language = language;
}
// 初始化离线 TTS 引擎
public boolean init() {
try {
// 1. 创建 TtsHelper 实例
ttsHelper = TtsHelper.getInstance(ability);
ttsHelper.init();
// 2. 配置离线模式(关键:禁用在线合成)
TtsEngine.Config config = new TtsEngine.Config();
config.setOnlineMode(false); // 强制离线模式
config.setLanguage(language); // 设置语言(如 "zh-CN", "en-US")
ttsHelper.setConfig(config);
// 3. 加载离线语音包(从 raw 目录或沙箱路径)
String offlineResourcePath = ability.getContext().getRawFileDir() + "/tts_offline_" + language + ".hap";
ttsHelper.loadOfflineResource(offlineResourcePath);
// 4. 创建 TtsPlayer 并设置回调
ttsPlayer = TtsPlayer.create(ability);
ttsPlayer.setTtsPlayerListener(new TtsPlayer.TtsPlayerListener() {
@Override
public void onPlayStart() {
HiLog.debug(LABEL, "TTS 播放开始");
}
@Override
public void onPlayPause() {
HiLog.debug(LABEL, "TTS 播放暂停");
}
@Override
public void onPlayStop() {
HiLog.debug(LABEL, "TTS 播放停止");
}
@Override
public void onPlayComplete() {
HiLog.debug(LABEL, "TTS 播放完成");
}
@Override
public void onError(int errorCode) {
HiLog.error(LABEL, "TTS 错误:errorCode = %{public}d", errorCode);
}
});
isInitialized = true;
return true;
} catch (Exception e) {
HiLog.error(LABEL, "TTS 初始化失败:%{public}s", e.getMessage());
return false;
}
}
// 语音合成与播放
public boolean speak(String text, double volume, double speed) {
if (!isInitialized || text.isEmpty()) {
return false;
}
try {
// 1. 配置音量(鸿蒙 TTS 音量范围:0-100)
int ttsVolume = (int) (volume * 100);
ttsPlayer.setVolume(ttsVolume);
// 2. 配置语速(鸿蒙 TTS 语速范围:0.5-2.0,与 Flutter 端一致)
ttsPlayer.setSpeed(speed);
// 3. 创建 TTS 请求
TtsRequest request = new TtsRequest();
request.setText(text);
request.setLanguage(language);
request.setOnlineMode(false); // 确保离线合成
// 4. 执行合成并播放
TtsResponse response = ttsHelper.generateTts(request);
if (response.getResultCode() == TtsResponse.SUCCESS) {
ttsPlayer.play(response.getAudioData());
return true;
} else {
HiLog.error(LABEL, "TTS 合成失败:resultCode = %{public}d", response.getResultCode());
return false;
}
} catch (Exception e) {
HiLog.error(LABEL, "TTS 播放失败:%{public}s", e.getMessage());
return false;
}
}
// 停止 TTS 播放
public void stop() {
if (ttsPlayer != null && ttsPlayer.isPlaying()) {
ttsPlayer.stop();
}
}
// 释放资源
public void release() {
if (ttsPlayer != null) {
ttsPlayer.release();
}
if (ttsHelper != null) {
ttsHelper.release();
}
isInitialized = false;
}
}
4.2 Flutter 端 TTS 调用示例
在 lib/main.dart 中实现 UI 界面与 TTS 功能调用:
dart
import 'package:flutter/material.dart';
import 'package:my_voice_app/voice_channel.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '离线语音交互 Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const VoiceHomePage(),
);
}
}
class VoiceHomePage extends StatefulWidget {
const VoiceHomePage({super.key});
@override
State<VoiceHomePage> createState() => _VoiceHomePageState();
}
class _VoiceHomePageState extends State<VoiceHomePage> {
final TextEditingController _ttsTextController = TextEditingController(
text: "Hello, 欢迎使用鸿蒙 Flutter 离线语音交互功能!",
);
double _volume = 0.8;
double _speed = 1.0;
String _language = "zh-CN";
bool _isEngineInitialized = false;
// 初始化离线引擎
Future<void> _initEngine() async {
final bool success = await VoiceChannel.initOfflineEngine(language: _language);
setState(() {
_isEngineInitialized = success;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(success ? "离线引擎初始化成功" : "离线引擎初始化失败")),
);
}
// 触发 TTS 合成
Future<void> _startTts() async {
if (!_isEngineInitialized) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("请先初始化离线引擎")),
);
return;
}
final bool success = await VoiceChannel.startTts(
text: _ttsTextController.text,
volume: _volume,
speed: _speed,
);
if (!success) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("TTS 合成失败,请检查配置")),
);
}
}
// 切换语言
void _changeLanguage(String newLanguage) {
setState(() {
_language = newLanguage;
_isEngineInitialized = false; // 切换语言后需重新初始化
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("离线 TTS/STT 演示")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 语言选择器
DropdownButton<String>(
value: _language,
items: const [
DropdownMenuItem(value: "zh-CN", child: Text("中文(普通话)")),
DropdownMenuItem(value: "en-US", child: Text("英文(美国)")),
DropdownMenuItem(value: "ja-JP", child: Text("日语")),
DropdownMenuItem(value: "ko-KR", child: Text("韩语")),
],
onChanged: (value) => _changeLanguage(value!),
hint: const Text("选择语言"),
),
const SizedBox(height: 16),
// 初始化引擎按钮
ElevatedButton(
onPressed: _initEngine,
child: const Text("初始化离线引擎"),
),
const SizedBox(height: 16),
// TTS 文本输入
TextField(
controller: _ttsTextController,
decoration: const InputDecoration(
labelText: "输入要合成的文本",
border: OutlineInputBorder(),
minLines: 3,
maxLines: 5,
),
),
const SizedBox(height: 16),
// 音量调节
Row(
children: [
const Text("音量:"),
Expanded(
child: Slider(
value: _volume,
min: 0.0,
max: 1.0,
divisions: 10,
label: "${(_volume * 100).toInt()}%",
onChanged: (value) => setState(() => _volume = value),
),
),
],
),
// 语速调节
Row(
children: [
const Text("语速:"),
Expanded(
child: Slider(
value: _speed,
min: 0.5,
max: 2.0,
divisions: 15,
label: "$_speedx",
onChanged: (value) => setState(() => _speed = value),
),
),
],
),
const SizedBox(height: 16),
// 开始合成按钮
Center(
child: ElevatedButton(
onPressed: _startTts,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
textStyle: const TextStyle(fontSize: 18),
),
child: const Text("开始语音合成"),
),
),
],
),
),
);
}
@override
void dispose() {
_ttsTextController.dispose();
super.dispose();
}
}
4.3 离线 TTS 关键配置说明
-
离线资源加载优先级 :
- 工程内置:
raw目录下的语音包优先加载,适用于固定语言需求; - 应用内下载:通过鸿蒙
DownloadManager下载语音包后,解压至getFilesDir()路径,适用于动态切换多语言场景。
- 工程内置:
-
语言代码规范 :遵循 ISO 639-1 语言代码 + ISO 3166-1 alpha-2 国家代码,如中文(zh-CN)、英文(en-US)。
-
权限配置 :在
harmonyos/src/main/config.json中添加以下权限:json
"module": { "abilities": [...], "reqPermissions": [ { "name": "ohos.permission.RECORD_AUDIO", // 录音权限(STT 需用) "reason": "用于离线语音识别", "usedScene": { "ability": ["com.example.voice.MainAbility"], "when": "inuse" } }, { "name": "ohos.permission.READ_MEDIA", // 读取媒体文件权限(加载离线资源) "reason": "用于加载离线语音包", "usedScene": { "ability": ["com.example.voice.MainAbility"], "when": "inuse" } } ] }
五、离线 STT 实现(PocketSphinx + 鸿蒙 NDK + Flutter 调用)
5.1 PocketSphinx 鸿蒙 NDK 编译
PocketSphinx 是开源的离线语音识别引擎,需通过鸿蒙 NDK 编译为 .so 库后集成到工程中:
5.1.1 编译环境准备
- 安装鸿蒙 NDK 工具链(DevEco Studio 中通过
Tools > HarmonyOS > SDK Manager下载); - 下载 PocketSphinx 源码:
git clone https://github.com/cmusphinx/pocketsphinx.git; - 创建鸿蒙 NDK 编译脚本
Android.mk和Application.mk(鸿蒙 NDK 兼容 Android NDK 语法)。
5.1.2 编译脚本配置(Android.mk)
makefile
LOCAL_PATH := $(call my-dir)
# 配置 PocketSphinx 源码路径
POCKETSPHINX_PATH := $(LOCAL_PATH)/pocketsphinx/src
# 编译 PocketSphinx 核心库
include $(CLEAR_VARS)
LOCAL_MODULE := pocketsphinx
LOCAL_SRC_FILES := \
$(POCKETSPHINX_PATH)/libpocketsphinx/align.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/decoder.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/feat.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/lattice.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/mfcc.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ngram_search.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/phone_loop_search.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_lattice.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_mllr.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_search.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_semseg.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_stats.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_alignment.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_decoder.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_features.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_language_model.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_mfcc.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_ngram.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_phone_loop.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_speech_detector.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/ps_util.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/string2int.c \
$(POCKETSPHINX_PATH)/libpocketsphinx/vad.c
# 配置编译选项
LOCAL_CFLAGS := \
-DHAVE_CONFIG_H \
-I$(POCKETSPHINX_PATH)/include \
-I$(POCKETSPHINX_PATH)/libpocketsphinx \
-O3 -fPIC
# 链接依赖库
LOCAL_LDLIBS := -llog -lm
include $(BUILD_SHARED_LIBRARY)
5.1.3 编译脚本配置(Application.mk)
makefile
# 支持的 CPU 架构(根据目标设备选择)
APP_ABI := arm64-v8a armeabi-v7a
# 鸿蒙 NDK 版本(需与下载的版本一致)
APP_PLATFORM := harmonyos-9
# C 语言标准
APP_CFLAGS := -std=c99
5.1.4 执行编译
bash
运行
# 进入 jni 目录
cd harmonyos/src/main/jni
# 执行 NDK 编译(替换为鸿蒙 NDK 路径)
/home/user/DevEcoStudio/ndk/26.1.10909125/ndk-build
编译成功后,会在 libs 目录生成 libpocketsphinx.so 文件,将其复制到 harmonyos/src/main/jniLibs 目录。
5.2 鸿蒙原生离线 STT 封装
创建 harmonyos/src/main/java/com/example/voice/OfflineSttManager.java,通过 JNI 调用 PocketSphinx 实现离线识别:
java
运行
import ohos.aafwk.ability.Ability;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.media.audio.AudioDeviceDescriptor;
import ohos.media.audio.AudioManager;
import ohos.media.audio.AudioRecord;
import ohos.media.audio.AudioStreamInfo;
import ohos.media.audio.AudioStreamType;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class OfflineSttManager {
private static final HiLogLabel LABEL = new HiLogLabel(HiLog.DEBUG, 0x00102, "OfflineSttManager");
private final Ability ability;
private final String language;
private AudioRecord audioRecord;
private boolean isRecording = false;
private boolean isInitialized = false;
private String modelPath; // 离线模型路径
// 加载 PocketSphinx 动态库
static {
System.loadLibrary("pocketsphinx");
}
// JNI 方法:初始化 PocketSphinx 引擎
private native boolean initPocketSphinx(String modelPath, String dictPath, String lmPath);
// JNI 方法:开始语音识别(传入音频数据)
private native String startRecognition(byte[] audioData, int length);
// JNI 方法:停止识别并释放资源
private native void stopRecognition();
// 构造函数
public OfflineSttManager(Ability ability, String language) {
this.ability = ability;
this.language = language;
this.modelPath = ability.getContext().getAssetsDir() + "/stt_models/";
}
// 初始化离线 STT 引擎
public boolean init() {
try {
// 1. 复制 assets 中的模型文件到沙箱路径(首次启动时执行)
copyAssetsToSandbox();
// 2. 配置模型路径(根据语言选择对应的模型)
String dictPath = modelPath + (language.equals("zh-CN") ? "mandarin_notone.dic" : "cmudict-en-us.dict");
String lmPath = modelPath + (language.equals("zh-CN") ? "zh-CN.lm.bin" : "en-us.lm.bin");
String acousticModelPath = modelPath + (language.equals("zh-CN") ? "zh_cn.cd_cont_5000" : "hub4wsj_sc_8k");
// 3. 初始化 PocketSphinx(JNI 调用)
boolean initSuccess = initPocketSphinx(acousticModelPath, dictPath, lmPath);
if (!initSuccess) {
HiLog.error(LABEL, "PocketSphinx 初始化失败");
return false;
}
// 4. 配置 AudioRecord(录音参数:16kHz 采样率、16bit 位宽、单声道)
AudioStreamInfo streamInfo = new AudioStreamInfo.Builder()
.setAudioStreamType(AudioStreamType.MEDIA)
.setSampleRate(16000) // PocketSphinx 推荐采样率
.setEncoding(AudioStreamInfo.Encoding.PCM_16BIT)
.setChannelMask(AudioStreamInfo.ChannelMask.CHANNEL_IN_MONO)
.build();
audioRecord = new AudioRecord(streamInfo);
isInitialized = true;
return true;
} catch (Exception e) {
HiLog.error(LABEL, "STT 初始化失败:%{public}s", e.getMessage());
return false;
}
}
// 复制 assets 中的模型文件到沙箱(避免 assets 目录不可写问题)
private void copyAssetsToSandbox() throws IOException {
File modelDir = new File(modelPath);
if (!modelDir.exists()) {
modelDir.mkdirs();
}
// 列出 assets 中的模型文件(需提前将模型放入 assets/stt_models 目录)
String[] modelFiles = ability.getContext().getAssets().list("stt_models");
if (modelFiles == null) return;
for (String fileName : modelFiles) {
File targetFile = new File(modelPath + fileName);
if (!targetFile.exists()) {
InputStream is = ability.getContext().getAssets().open("stt_models/" + fileName);
OutputStream os = new java.io.FileOutputStream(targetFile);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
is.close();
os.close();
}
}
}
// 开始语音识别(同步阻塞调用,建议在子线程中执行)
public String recognize() {
if (!isInitialized) {
HiLog.error(LABEL, "STT 引擎未初始化");
return null;
}
try {
isRecording = true;
audioRecord.startRecording();
HiLog.debug(LABEL, "开始录音识别");
// 缓冲区大小(16kHz 采样率、16bit 位宽、单声道:1秒 = 32000 字节)
int bufferSize = 32000;
byte[] buffer = new byte[bufferSize];
StringBuilder result = new StringBuilder();
// 录制 5 秒音频(可根据需求调整时长)
long startTime = System.currentTimeMillis();
while (isRecording && System.currentTimeMillis() - startTime < 5000) {
int readLen = audioRecord.read(buffer, 0, bufferSize);
if (readLen > 0) {
// 调用 JNI 识别音频数据
String partialResult = startRecognition(buffer, readLen);
if (partialResult != null && !partialResult.isEmpty()) {
result.append(partialResult);
}
}
}
stop();
return result.toString().trim();
} catch (Exception e) {
HiLog.error(LABEL, "识别失败:%{public}s", e.getMessage());
stop();
return null;
}
}
// 停止识别
public void stop() {
isRecording = false;
if (audioRecord != null && audioRecord.getRecordingState() == AudioRecord.RecordingState.RECORDING) {
audioRecord.stop();
}
stopRecognition(); // JNI 释放资源
HiLog.debug(LABEL, "停止录音识别");
}
// 释放资源
public void release() {
stop();
if (audioRecord != null) {
audioRecord.release();
}
isInitialized = false;
}
}
5.3 JNI 实现(PocketSphinx 调用)
创建 harmonyos/src/main/jni/pocketsphinx_jni.c,实现 JNI 方法与 PocketSphinx 的交互:
c
运行
#include <jni.h>
#include <string.h>
#include <android/log.h>
#include <pocketsphinx.h>
#define LOG_TAG "PocketSphinxJNI"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// 全局变量:PocketSphinx 解码器
static ps_decoder_t *ps = NULL;
static cmd_ln_t *config = NULL;
// 初始化 PocketSphinx 引擎
JNIEXPORT jboolean JNICALL
Java_com_example_voice_OfflineSttManager_initPocketSphinx(
JNIEnv *env, jobject thiz,
jstring acoustic_model_path,
jstring dict_path,
jstring lm_path) {
// 转换 Java 字符串为 C 字符串
const char *am_path = (*env)->GetStringUTFChars(env, acoustic_model_path, NULL);
const char *dict_path_c = (*env)->GetStringUTFChars(env, dict_path, NULL);
const char *lm_path_c = (*env)->GetStringUTFChars(env, lm_path, NULL);
// 配置 PocketSphinx 参数
config = cmd_ln_init(NULL, ps_args(), TRUE,
"-hmm", am_path, // 声学模型路径
"-dict", dict_path_c, // 字典路径
"-lm", lm_path_c, // 语言模型路径
"-samplerate", "16000", // 采样率(与 AudioRecord 一致)
"-nfft", "512", // FFT 大小
NULL);
if (config == NULL) {
LOGE("PocketSphinx 配置初始化失败");
return JNI_FALSE;
}
// 创建解码器
ps = ps_init(config);
if (ps == NULL) {
LOGE("PocketSphinx 解码器初始化失败");
cmd_ln_free_r(config);
return JNI_FALSE;
}
// 释放字符串资源
(*env)->ReleaseStringUTFChars(env, acoustic_model_path, am_path);
(*env)->ReleaseStringUTFChars(env, dict_path, dict_path_c);
(*env)->ReleaseStringUTFChars(env, lm_path, lm_path_c);
LOGD("PocketSphinx 初始化成功");
return JNI_TRUE;
}
// 开始语音识别
JNIEXPORT jstring JNICALL
Java_com_example_voice_OfflineSttManager_startRecognition(
JNIEnv *env, jobject thiz,
jbyteArray audio_data,
jint length) {
if (ps == NULL) {
LOGE("解码器未初始化");
return NULL;
}
// 获取音频数据
jbyte *audio_bytes = (*env)->GetByteArrayElements(env, audio_data, NULL);
if (audio_bytes == NULL) {
LOGE("获取音频数据失败");
return NULL;
}
// 开始识别(PocketSphinx 要求音频为 16bit PCM 格式)
if (ps_start_utt(ps) < 0) {
LOGE("开始识别失败");
(*env)->ReleaseByteArrayElements(env, audio_data, audio_bytes, JNI_ABORT);
return NULL;
}
// 处理音频数据
ps_process_raw(ps, (const int16 *) audio_bytes, length / 2, FALSE, FALSE);
// 结束识别并获取结果
ps_end_utt(ps);
char *hyp = ps_get_hyp(ps, NULL);
// 释放资源
(*env)->ReleaseByteArrayElements(env, audio_data, audio_bytes, JNI_ABORT);
if (hyp == NULL) {
LOGD("未识别到语音");
return NULL;
}
// 转换 C 字符串为 Java 字符串
jstring result = (*env)->NewStringUTF(env, hyp);
ckd_free(hyp); // 释放 PocketSphinx 结果内存
return result;
}
// 停止识别并释放资源
JNIEXPORT void JNICALL
Java_com_example_voice_OfflineSttManager_stopRecognition(
JNIEnv *env, jobject thiz) {
if (ps != NULL) {
ps_free(ps);
ps = NULL;
}
if (config != NULL) {
cmd_ln_free_r(config);
config = NULL;
}
LOGD("PocketSphinx 资源释放成功");
}
5.4 Flutter 端 STT 调用示例
在 VoiceHomePage 中添加 STT 识别相关 UI 与逻辑:
dart
// 在 _VoiceHomePageState 中添加以下成员变量
String? _sttResult;
bool _isRecognizing = false;
// 开始 STT 识别
Future<void> _startStt() async {
if (!_isEngineInitialized) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("请先初始化离线引擎")),
);
return;
}
if (_isRecognizing) return;
setState(() {
_isRecognizing = true;
_sttResult = null;
});
// 调用 STT 识别(建议在子线程中执行,避免阻塞 UI)
final String? result = await compute(_callStt, null);
setState(() {
_isRecognizing = false;
_sttResult = result ?? "未识别到语音";
});
}
// 子线程执行 STT 调用(避免 UI 阻塞)
static String? _callStt(void _) {
return VoiceChannel.startStt();
}
// 在 build 方法中添加 STT 相关 UI
Column(
children: [
// ... 原有 TTS 相关 UI ...
const Divider(height: 32),
const Text("离线语音识别", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
// 开始识别按钮
ElevatedButton(
onPressed: _isRecognizing ? null : _startStt,
style: ElevatedButton.styleFrom(
backgroundColor: _isRecognizing ? Colors.grey : Colors.green,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
textStyle: const TextStyle(fontSize: 18),
),
child: Text(_isRecognizing ? "识别中..." : "开始语音识别"),
),
const SizedBox(height: 16),
// 识别结果展示
if (_sttResult != null)
Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
"识别结果:$_sttResult",
style: const TextStyle(fontSize: 16),
),
),
),
],
)
六、多语言适配进阶
6.1 多语言资源管理
-
离线资源按需加载:
-
工程内置常用语言(如中文、英文)的语音包 / 模型,减少初始安装包体积;
-
其他语言(如日语、韩语)通过应用内下载模块动态获取,下载后解压至沙箱路径;
-
示例:Flutter 端语言下载逻辑(
lib/services/download_service.dart):dart
import 'dart:io'; import 'package:dio/dio.dart'; import 'package:path_provider/path_provider.dart'; class DownloadService { static final Dio _dio = Dio(); // 下载离线语音包(TTS) static Future<String?> downloadTtsResource(String language) async { try { // 语音包下载地址(需自行搭建服务器) final String url = "https://example.com/tts_offline_$language.hap"; final Directory appDir = await getApplicationDocumentsDirectory(); final String savePath = "${appDir.path}/tts_offline_$language.hap"; // 检查是否已下载 final File file = File(savePath); if (await file.exists()) { return savePath; } // 下载文件 await _dio.download( url, savePath, onReceiveProgress: (received, total) { final progress = (received / total * 100).toStringAsFixed(1); print("TTS 资源下载进度:$progress%"); }, ); return savePath; } catch (e) { print("下载 TTS 资源失败:$e"); return null; } } // 下载离线模型(STT) static Future<bool> downloadSttModel(String language) async { // 类似 TTS 下载逻辑,下载对应的语言模型、字典、声学模型文件 // ... } }
-
-
语言切换逻辑优化:
-
切换语言时,先检查本地是否存在对应离线资源,不存在则触发下载;
-
下载完成后,重新初始化 TTS/STT 引擎,确保资源加载生效;
-
示例:优化
_changeLanguage方法:dart
void _changeLanguage(String newLanguage) async { setState(() { _language = newLanguage; _isEngineInitialized = false; _isLoading = true; // 显示加载状态 }); // 检查并下载 TTS 离线资源 final String? ttsResourcePath = await DownloadService.downloadTtsResource(newLanguage); if (ttsResourcePath == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("下载 $newLanguage 语音包失败")), ); setState(() => _isLoading = false); return; } // 检查并下载 STT 离线模型 final bool sttModelReady = await DownloadService.downloadSttModel(newLanguage); if (!sttModelReady) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("下载 $newLanguage 识别模型失败")), ); setState(() => _isLoading = false); return; } // 初始化引擎 await _initEngine(); setState(() => _isLoading = false); }
-
6.2 语言适配细节
- 文本编码处理:多语言文本需使用 UTF-8 编码,避免中文、日语等字符乱码;
- 语音包版本兼容 :确保下载的离线语音包版本与鸿蒙 SDK 版本匹配(参考 鸿蒙语音包版本说明);
- 识别词典扩展 :针对特定场景(如行业术语、专有名词),可自定义 PocketSphinx 词典文件(
.dic),提升识别准确率。
七、性能优化与避坑指南
7.1 性能优化策略
-
资源占用优化:
-
离线语音包 / 模型压缩:使用
gzip压缩资源文件,下载后解压,减少存储空间占用; -
按需释放资源:应用退到后台时,释放 TTS/STT 引擎资源,前台唤醒时重新初始化;
-
示例:鸿蒙端资源释放逻辑(
MainAbility.java):java
运行
@Override protected void onBackground() { super.onBackground(); // 释放 TTS/STT 资源 if (ttsManager != null) { ttsManager.release(); } if (sttManager != null) { sttManager.release(); } } @Override protected void onForeground(Intent intent) { super.onForeground(intent); // 重新初始化引擎 if (ttsManager != null && sttManager != null) { ttsManager.init(); sttManager.init(); } }
-
-
响应速度优化:
- TTS 预加载:启动应用时预加载常用语言的 TTS 引擎,减少首次合成延迟;
- STT 音频缓冲区优化:调整
AudioRecord缓冲区大小(建议 16kHz 采样率下使用 32000 字节缓冲区),平衡延迟与识别准确率; - 异步处理:所有引擎初始化、语音合成 / 识别操作均在子线程执行,避免阻塞 UI 线程。
7.2 常见问题与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| TTS 合成无声音 | 1. 离线语音包未加载;2. 音量设置为 0;3. 权限未授予 | 1. 检查语音包路径是否正确;2. 确保音量 > 0;3. 动态申请 READ_MEDIA 权限 |
| STT 识别准确率低 | 1. 模型文件不匹配;2. 采样率与引擎配置不一致;3. 环境噪音大 | 1. 确认模型与语言对应;2. 统一采样率为 16kHz;3. 开启 VAD(语音活动检测) |
| 应用启动慢 | 离线资源过大,初始化耗时过长 | 1. 拆分资源,按需加载;2. 异步初始化引擎,显示启动页 |
崩溃报错:UnsatisfiedLinkError |
PocketSphinx .so 库未正确加载 |
1. 检查 jniLibs 目录结构;2. 确保 CPU 架构匹配;3. 重新编译 .so 库 |
| 多语言切换后引擎初始化失败 | 新语言资源未下载完成或路径错误 | 1. 切换前检查资源完整性;2. 打印资源路径日志,排查路径错误 |
八、总结与展望
本文详细介绍了鸿蒙 + Flutter 混合开发架构下,TTS/STT 全离线部署与多语言适配的实现方案,核心亮点包括:
- 基于 MethodChannel 实现 Flutter 与鸿蒙原生的高效通信,兼顾跨平台一致性与原生性能;
- 整合鸿蒙原生 TTS 引擎与 PocketSphinx 开源 STT 引擎,实现全场景离线语音交互;
- 提供多语言资源按需加载、动态切换方案,适配全球化应用需求;
- 包含完整的代码示例、编译脚本与避坑指南,降低开发者落地成本。
未来优化方向
- 识别准确率提升:集成更先进的离线 STT 引擎(如 Whisper 轻量版),通过模型量化减少资源占用;
- 语音唤醒功能:添加离线语音唤醒(如 "小艺小艺"),实现零交互启动语音交互;
- 个性化定制:支持用户自定义语音合成音色、识别词典,适配垂直行业场景;
- 分布式语音交互:利用鸿蒙分布式能力,实现多设备间的离线语音协同(如手机录音、平板识别)。
参考资料
本文代码已上传至 GitHub 仓库:harmonyos-flutter-voice-offline,欢迎 Star 关注!如有问题或优化建议,欢迎在评论区留言交流~


