Browse Source

add

newdev_shunjiawei
wolfplus 2 years ago
parent
commit
e09bcf2d8b
  1. 4
      android/app/build.gradle.kts
  2. 6
      android/app/src/main/AndroidManifest.xml
  3. 19
      android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt
  4. 293
      android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt
  5. 12
      android/settings.gradle.kts
  6. 2
      lib/core/bindings/initial_binding.dart
  7. 5
      lib/core/routes/app_pages.dart
  8. 1
      lib/core/routes/app_routes.dart
  9. 264
      lib/data/services/voice_recognition_service.dart
  10. 137
      lib/modules/speech_demo/speech_demo_page.dart

4
android/app/build.gradle.kts

@ -1,3 +1,5 @@
plugins { plugins {
id("com.android.application") id("com.android.application")
id("kotlin-android") id("kotlin-android")
@ -43,6 +45,8 @@ android {
dependencies { dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
// 火山语音识别 SDK 依赖
implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.6")
} }
flutter { flutter {

6
android/app/src/main/AndroidManifest.xml

@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 添加必要的权限 --> <!-- 添加必要的权限 -->
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
@ -12,7 +13,8 @@
<application <application
android:label="deep_voice" android:label="deep_voice"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher"
tools:replace="android:label">
<!-- Audio Service --> <!-- Audio Service -->
<service android:name="com.ryanheise.audioservice.AudioService" <service android:name="com.ryanheise.audioservice.AudioService"

19
android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt

@ -1,5 +1,22 @@
package com.example.deep_voice package com.example.deep_voice
import com.ryanheise.audioservice.AudioServiceActivity import com.ryanheise.audioservice.AudioServiceActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: AudioServiceActivity() class MainActivity: AudioServiceActivity() {
private var volcSpeechBridge: VolcSpeechBridge? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 初始化火山语音识别桥接类
volcSpeechBridge = VolcSpeechBridge(this, flutterEngine.dartExecutor.binaryMessenger)
}
override fun onDestroy() {
// 释放火山语音识别资源
volcSpeechBridge?.dispose()
volcSpeechBridge = null
super.onDestroy()
}
}

293
android/app/src/main/kotlin/com/example/deep_voice/VolcSpeechBridge.kt

@ -0,0 +1,293 @@
package com.example.deep_voice
import android.app.Activity
import android.content.Context
import android.util.Log
import com.bytedance.speech.speechengine.SpeechEngine
import com.bytedance.speech.speechengine.SpeechEngineDefines
import com.bytedance.speech.speechengine.SpeechEngineGenerator
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import org.json.JSONObject
/**
* 火山语音识别桥接类
* 负责与火山语音识别 SDK 交互,并将结果通过 MethodChannel 和 EventChannel 传递给 Flutter 层
*/
class VolcSpeechBridge(
private val activity: Activity,
messenger: BinaryMessenger
) : SpeechEngine.SpeechListener {
companion object {
private const val TAG = "VolcSpeechBridge"
private const val CHANNEL_NAME = "com.example.deep_voice/speech_recognition"
private const val EVENT_CHANNEL_NAME = "com.example.deep_voice/speech_recognition_events"
// 定义指令常量
private const val DIRECTIVE_START_RECOGNITION = 1
private const val DIRECTIVE_STOP_RECOGNITION = 2
}
private var engine: SpeechEngine? = null
private var methodResult: Result? = null
private var eventSink: EventChannel.EventSink? = null
private var isInitialized = false
// 初始化 MethodChannel 和 EventChannel
init {
// 设置 MethodChannel 处理方法调用
MethodChannel(messenger, CHANNEL_NAME).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val appKey = call.argument<String>("appKey") ?: ""
val accessKey = call.argument<String>("accessKey") ?: ""
initialize(appKey, accessKey, result)
}
"startRecognition" -> {
startRecognition(result)
}
"stopRecognition" -> {
stopRecognition(result)
}
"isInitialized" -> {
result.success(isInitialized)
}
else -> {
result.notImplemented()
}
}
}
// 设置 EventChannel 处理事件流
EventChannel(messenger, EVENT_CHANNEL_NAME).setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
})
}
/**
* 初始化火山语音识别引擎
*/
fun initialize(appKey: String, accessKey: String, result: Result) {
methodResult = result
try {
Log.i(TAG, "Initializing Volcengine Speech Engine")
// 创建引擎实例
engine = SpeechEngineGenerator.getInstance()
engine?.createEngine()
// 设置监听器
engine?.setListener(this)
// 设置应用 ID 和访问密钥
engine?.setOptionString("app_id", appKey)
engine?.setOptionString("access_key", accessKey)
// 设置语音识别参数
engine?.setOptionBoolean("enable_punctuation", true) // 启用标点符号
engine?.setOptionBoolean("enable_intermediate_result", true) // 启用中间结果
engine?.setOptionBoolean("enable_vad", true) // 启用语音活动检测
// 设置语言(普通话)
engine?.setOptionString("language", "zh")
// 初始化引擎
val ret = engine?.initEngine()
if (ret == SpeechEngineDefines.ERR_NO_ERROR) {
isInitialized = true
Log.i(TAG, "Volcengine Speech Engine initialized successfully")
result.success("Engine initialized")
// 发送状态更新到 Flutter
sendEvent("status", "initialized")
} else {
Log.e(TAG, "Engine initialization failed with error code: $ret")
result.error("INIT_ERROR", "Engine initialization failed with error code: $ret", null)
}
} catch (e: Exception) {
Log.e(TAG, "Initialization failed", e)
result.error("INIT_ERROR", e.message, null)
}
}
/**
* 启动语音识别
*/
fun startRecognition(result: Result) {
try {
if (!isInitialized) {
result.error("NOT_INITIALIZED", "Speech engine is not initialized", null)
return
}
Log.i(TAG, "Starting speech recognition")
val ret = engine?.sendDirective(DIRECTIVE_START_RECOGNITION, "")
if (ret == SpeechEngineDefines.ERR_NO_ERROR) {
result.success("Recognition started")
// 发送状态更新到 Flutter
sendEvent("status", "listening")
} else {
Log.e(TAG, "Start recognition failed with error code: $ret")
result.error("START_ERROR", "Start recognition failed with error code: $ret", null)
}
} catch (e: Exception) {
Log.e(TAG, "Start recognition failed", e)
result.error("START_ERROR", e.message, null)
}
}
/**
* 停止语音识别
*/
fun stopRecognition(result: Result) {
try {
if (!isInitialized) {
result.error("NOT_INITIALIZED", "Speech engine is not initialized", null)
return
}
Log.i(TAG, "Stopping speech recognition")
val ret = engine?.sendDirective(DIRECTIVE_STOP_RECOGNITION, "")
if (ret == SpeechEngineDefines.ERR_NO_ERROR) {
result.success("Recognition stopped")
// 发送状态更新到 Flutter
sendEvent("status", "idle")
} else {
Log.e(TAG, "Stop recognition failed with error code: $ret")
result.error("STOP_ERROR", "Stop recognition failed with error code: $ret", null)
}
} catch (e: Exception) {
Log.e(TAG, "Stop recognition failed", e)
result.error("STOP_ERROR", e.message, null)
}
}
/**
* 释放资源
*/
fun dispose() {
try {
engine?.destroyEngine()
engine = null
isInitialized = false
Log.i(TAG, "Volcengine Speech Engine released")
} catch (e: Exception) {
Log.e(TAG, "Error releasing engine", e)
}
}
// ------------------
// 发送事件到 Flutter
// ------------------
private fun sendEvent(eventName: String, data: Any) {
try {
val eventData = JSONObject().apply {
put("event", eventName)
put("data", data)
}
eventSink?.success(eventData.toString())
} catch (e: Exception) {
Log.e(TAG, "Error sending event", e)
}
}
// ------------------
// SpeechListener 接口回调
// ------------------
/**
* 处理语音消息回调
* 这是 SpeechListener 接口的必须实现方法
*/
override fun onSpeechMessage(messageType: Int, messageData: ByteArray?, messageLength: Int) {
try {
when (messageType) {
// 初始化完成消息
1 -> {
Log.i(TAG, "Engine initialized successfully")
isInitialized = true
// 通知 Flutter 初始化完成
methodResult?.success("Engine initialized")
methodResult = null
// 发送状态更新到 Flutter
sendEvent("status", "initialized")
}
// 错误消息
2 -> {
val errorCode = messageData?.let { String(it, 0, messageLength).toIntOrNull() } ?: -1
Log.e(TAG, "Engine error: $errorCode")
// 通知 Flutter 识别出错
val errorData = JSONObject().apply {
put("code", errorCode)
put("message", "Error code: $errorCode")
}
sendEvent("error", errorData.toString())
}
// VAD 状态变化消息
3 -> {
val state = messageData?.let { String(it, 0, messageLength).toIntOrNull() } ?: -1
Log.i(TAG, "VAD state changed: $state")
val vadState = when (state) {
0 -> "silence"
1 -> "speech"
else -> "unknown"
}
sendEvent("vad", vadState)
}
// 识别结果消息
4 -> {
messageData?.let {
val resultJson = String(it, 0, messageLength)
try {
val jsonObject = JSONObject(resultJson)
val text = jsonObject.optString("text", "")
val isFinal = jsonObject.optBoolean("is_final", false)
Log.i(TAG, "Recognition result: $text, isFinal: $isFinal")
// 发送识别结果到 Flutter
val resultData = JSONObject().apply {
put("text", text)
put("isFinal", isFinal)
}
sendEvent("result", resultData.toString())
} catch (e: Exception) {
Log.e(TAG, "Error parsing result JSON", e)
}
}
}
else -> {
Log.d(TAG, "Received unknown message type: $messageType")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error processing speech message", e)
}
}
}

12
android/settings.gradle.kts

@ -12,6 +12,9 @@ pluginManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven {
url = uri("https://artifact.bytedance.com/repository/Volcengine/")
}
gradlePluginPortal() gradlePluginPortal()
} }
} }
@ -22,4 +25,13 @@ plugins {
id("org.jetbrains.kotlin.android") version "1.8.22" apply false id("org.jetbrains.kotlin.android") version "1.8.22" apply false
} }
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven { url = uri("https://artifact.bytedance.com/repository/Volcengine/") }
gradlePluginPortal()
}
}
include(":app") include(":app")

2
lib/core/bindings/initial_binding.dart

@ -4,6 +4,7 @@ import '../../data/services/volcano_tts_service.dart';
import '../../data/services/audio_service.dart'; import '../../data/services/audio_service.dart';
import '../../data/services/notification_service.dart'; import '../../data/services/notification_service.dart';
import '../../data/services/volcano_ai_service.dart'; import '../../data/services/volcano_ai_service.dart';
import '../../data/services/voice_recognition_service.dart';
/// 初始绑定,用于管理全局依赖 /// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings { class InitialBinding extends Bindings {
@ -14,6 +15,7 @@ class InitialBinding extends Bindings {
final volcanoTtsService = Get.put(VolcanoTtsService(), permanent: true); final volcanoTtsService = Get.put(VolcanoTtsService(), permanent: true);
final audioService = Get.put(AudioServiceManager(), permanent: true); final audioService = Get.put(AudioServiceManager(), permanent: true);
final volcanoAiService = Get.put(VolcanoAIService(), permanent: true); final volcanoAiService = Get.put(VolcanoAIService(), permanent: true);
final voiceRecognitionService = Get.put(VoiceRecognitionService(), permanent: true);
// 只注册全局控制器 // 只注册全局控制器
Get.put(PermissionController(), permanent: true); Get.put(PermissionController(), permanent: true);

5
lib/core/routes/app_pages.dart

@ -7,6 +7,7 @@ import '../../modules/profile/views/profile_view.dart';
import '../../modules/profile/bindings/profile_binding.dart'; import '../../modules/profile/bindings/profile_binding.dart';
import '../../modules/chat/bindings/chat_binding.dart'; import '../../modules/chat/bindings/chat_binding.dart';
import '../../modules/chat/views/chat_view.dart'; import '../../modules/chat/views/chat_view.dart';
import '../../modules/speech_demo/speech_demo_page.dart';
import './app_routes.dart'; import './app_routes.dart';
abstract class AppPages { abstract class AppPages {
@ -31,5 +32,9 @@ abstract class AppPages {
page: () => const ProfileView(), page: () => const ProfileView(),
binding: ProfileBinding(), binding: ProfileBinding(),
), ),
GetPage(
name: Routes.speechDemo,
page: () => const SpeechDemoPage(),
),
]; ];
} }

1
lib/core/routes/app_routes.dart

@ -4,4 +4,5 @@ abstract class Routes {
static const explore = '/explore'; static const explore = '/explore';
static const chat = '/chat'; static const chat = '/chat';
static const profile = '/profile'; static const profile = '/profile';
static const speechDemo = '/speech_demo';
} }

264
lib/data/services/voice_recognition_service.dart

@ -0,0 +1,264 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 火山语音识别服务
/// 负责与原生层的火山语音识别引擎交互
class VoiceRecognitionService extends GetxService {
// 方法通道,用于调用原生方法
static const MethodChannel _methodChannel =
MethodChannel('com.example.deep_voice/speech_recognition');
// 事件通道,用于接收原生层的事件
static const EventChannel _eventChannel =
EventChannel('com.example.deep_voice/speech_recognition_events');
// 状态变量
final isInitialized = false.obs;
final isListening = false.obs;
final isConnecting = true.obs;
// 识别结果
final recognizedText = ''.obs;
// 回调函数
Function(String)? onRecognitionResult;
Function(String)? onError;
Function()? onRecognitionComplete;
Function(String)? onConnectionStatusChanged;
Function(String)? onSentenceComplete;
// 事件流订阅
StreamSubscription? _eventSubscription;
// 火山语音识别的 AppKey 和 AccessKey
late final String _appKey;
late final String _accessKey;
@override
void onInit() {
super.onInit();
_loadCredentials();
_setupEventListener();
}
/// 加载火山语音识别的凭证
void _loadCredentials() {
try {
_appKey = dotenv.env['VOLC_SPEECH_APP_KEY'] ?? '';
_accessKey = dotenv.env['VOLC_SPEECH_ACCESS_KEY'] ?? '';
if (_appKey.isEmpty || _accessKey.isEmpty) {
debugPrint('警告: 火山语音识别凭证未设置,请在 .env 文件中设置 VOLC_SPEECH_APP_KEY 和 VOLC_SPEECH_ACCESS_KEY');
}
} catch (e) {
debugPrint('加载火山语音识别凭证失败: $e');
}
}
/// 设置事件监听器
void _setupEventListener() {
try {
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
(dynamic event) {
_handleEvent(event.toString());
},
onError: (dynamic error) {
debugPrint('事件通道错误: $error');
if (onError != null) {
onError!('事件通道错误: $error');
}
},
);
} catch (e) {
debugPrint('设置事件监听器失败: $e');
}
}
/// 处理来自原生层的事件
void _handleEvent(String eventJson) {
try {
final eventData = jsonDecode(eventJson);
final eventName = eventData['event'];
final data = eventData['data'];
switch (eventName) {
case 'status':
_handleStatusEvent(data.toString());
break;
case 'result':
_handleResultEvent(data.toString());
break;
case 'error':
_handleErrorEvent(data.toString());
break;
case 'vad':
_handleVadEvent(data.toString());
break;
}
} catch (e) {
debugPrint('处理事件失败: $e');
}
}
/// 处理状态事件
void _handleStatusEvent(String status) {
switch (status) {
case 'initialized':
isInitialized.value = true;
isConnecting.value = false;
break;
case 'listening':
isListening.value = true;
break;
case 'idle':
isListening.value = false;
break;
}
if (onConnectionStatusChanged != null) {
onConnectionStatusChanged!(status);
}
}
/// 处理识别结果事件
void _handleResultEvent(String resultJson) {
try {
final resultData = jsonDecode(resultJson);
final text = resultData['text'];
final isFinal = resultData['isFinal'];
// 更新识别文本
recognizedText.value = text;
// 调用回调函数
if (onRecognitionResult != null) {
onRecognitionResult!(text);
}
// 如果是最终结果,调用完成回调
if (isFinal && text.isNotEmpty) {
if (onSentenceComplete != null) {
onSentenceComplete!(text);
}
if (onRecognitionComplete != null) {
onRecognitionComplete!();
}
}
} catch (e) {
debugPrint('处理识别结果失败: $e');
}
}
/// 处理错误事件
void _handleErrorEvent(String errorJson) {
try {
final errorData = jsonDecode(errorJson);
final errorMessage = errorData['message'];
debugPrint('语音识别错误: $errorMessage');
if (onError != null) {
onError!(errorMessage);
}
} catch (e) {
debugPrint('处理错误事件失败: $e');
}
}
/// 处理 VAD 事件
void _handleVadEvent(String vadState) {
// 可以根据需要处理 VAD 状态变化
debugPrint('VAD 状态: $vadState');
}
/// 初始化语音识别引擎
Future<void> initialize() async {
try {
isConnecting.value = true;
// 检查凭证
if (_appKey.isEmpty || _accessKey.isEmpty) {
throw '火山语音识别凭证未设置';
}
// 调用原生方法初始化引擎
final result = await _methodChannel.invokeMethod<String>(
'initialize',
{
'appKey': _appKey,
'accessKey': _accessKey,
},
);
debugPrint('初始化结果: $result');
// 初始化成功后,状态会通过事件通道更新
} catch (e) {
isConnecting.value = false;
debugPrint('初始化语音识别引擎失败: $e');
if (onError != null) {
onError!('初始化失败: $e');
}
rethrow;
}
}
/// 开始语音识别
Future<void> startRecognition() async {
try {
// 检查是否已初始化
if (!isInitialized.value) {
await initialize();
}
// 调用原生方法开始识别
final result = await _methodChannel.invokeMethod<String>('startRecognition');
debugPrint('开始识别结果: $result');
// 状态会通过事件通道更新
} catch (e) {
debugPrint('开始语音识别失败: $e');
if (onError != null) {
onError!('开始识别失败: $e');
}
rethrow;
}
}
/// 停止语音识别
Future<void> stopRecognition() async {
try {
// 检查是否已初始化
if (!isInitialized.value) {
return;
}
// 调用原生方法停止识别
final result = await _methodChannel.invokeMethod<String>('stopRecognition');
debugPrint('停止识别结果: $result');
// 状态会通过事件通道更新
} catch (e) {
debugPrint('停止语音识别失败: $e');
if (onError != null) {
onError!('停止识别失败: $e');
}
}
}
@override
void onClose() {
// 取消事件订阅
_eventSubscription?.cancel();
// 停止识别
stopRecognition();
super.onClose();
}
}

137
lib/modules/speech_demo/speech_demo_page.dart

@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../data/services/voice_recognition_service.dart';
class SpeechDemoPage extends StatefulWidget {
const SpeechDemoPage({Key? key}) : super(key: key);
@override
_SpeechDemoPageState createState() => _SpeechDemoPageState();
}
class _SpeechDemoPageState extends State<SpeechDemoPage> {
final VoiceRecognitionService _recognitionService = Get.find<VoiceRecognitionService>();
@override
void initState() {
super.initState();
_initializeEngine();
}
Future<void> _initializeEngine() async {
try {
await _recognitionService.initialize();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('初始化失败: $e')),
);
}
}
}
Future<void> _startRecognition() async {
try {
await _recognitionService.startRecognition();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('启动识别失败: $e')),
);
}
}
}
Future<void> _stopRecognition() async {
try {
await _recognitionService.stopRecognition();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('停止识别失败: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('火山语音识别演示'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 状态显示
Obx(() {
String status = '准备中';
if (_recognitionService.isConnecting.value) {
status = '连接中...';
} else if (_recognitionService.isInitialized.value) {
status = _recognitionService.isListening.value ? '正在听...' : '就绪';
}
return Text('状态: $status', style: const TextStyle(fontSize: 18));
}),
const SizedBox(height: 20),
// 识别结果显示
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
width: double.infinity,
height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(16),
child: Obx(() {
return SingleChildScrollView(
child: Text(
_recognitionService.recognizedText.value,
style: const TextStyle(fontSize: 16),
),
);
}),
),
),
const SizedBox(height: 20),
// 控制按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() {
return ElevatedButton(
onPressed: _recognitionService.isInitialized.value && !_recognitionService.isListening.value
? _startRecognition
: null,
child: const Text('开始识别'),
);
}),
const SizedBox(width: 20),
Obx(() {
return ElevatedButton(
onPressed: _recognitionService.isListening.value
? _stopRecognition
: null,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text('停止识别'),
);
}),
],
),
],
),
),
);
}
}
Loading…
Cancel
Save