Browse Source

Merge commit '99d5f41ab3794d55993b9f9d9f5c00e4b2095eb1' into lxm

newdev_shunjiawei
lxm 2 years ago
parent
commit
66b426588a
  1. 79
      .cursorrules
  2. 12
      .mcp.json
  3. 33
      amap_flutter/.gitignore
  4. 33
      amap_flutter/.metadata
  5. 3
      amap_flutter/CHANGELOG.md
  6. 1
      amap_flutter/LICENSE
  7. 15
      amap_flutter/README.md
  8. 4
      amap_flutter/analysis_options.yaml
  9. 9
      amap_flutter/android/.gitignore
  10. 54
      amap_flutter/android/build.gradle
  11. 1
      amap_flutter/android/settings.gradle
  12. 3
      amap_flutter/android/src/main/AndroidManifest.xml
  13. 168
      amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java
  14. 38
      amap_flutter/ios/.gitignore
  15. 0
      amap_flutter/ios/Assets/.gitkeep
  16. 19
      amap_flutter/ios/Classes/AmapFlutterPlugin.swift
  17. 14
      amap_flutter/ios/Resources/PrivacyInfo.xcprivacy
  18. 29
      amap_flutter/ios/amap_flutter.podspec
  19. 36
      amap_flutter/lib/amap_flutter.dart
  20. 45
      amap_flutter/lib/amap_flutter_method_channel.dart
  21. 49
      amap_flutter/lib/amap_flutter_platform_interface.dart
  22. 72
      amap_flutter/pubspec.yaml
  23. 45
      android/app/build.gradle.kts
  24. 35
      android/app/src/main/AndroidManifest.xml
  25. 654
      android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt
  26. 344
      android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt
  27. 0
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/ClassicBluetoothHelper.kt
  28. 382
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  29. 415
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt
  30. 383
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt
  31. 0
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/core/utils/FileLogger.kt
  32. 19
      android/build.gradle.kts
  33. 20
      android/settings.gradle.kts
  34. 21
      azure/LICENSE
  35. 66
      azure/README.md
  36. 757
      azure/ios/Classes/AzureAsrHelper.swift
  37. 18
      azure/ios/Classes/AzureSpeechRecognitionPlugin.swift
  38. 427
      azure/ios/Classes/AzureTtsHelper.swift
  39. 259
      azure/ios/Classes/SwiftAzureSpeechRecognitionPlugin.swift
  40. 24
      azure/ios/azure_speech_recognition.podspec
  41. 6
      azure/lib/azure_speech_recognition.dart
  42. 23
      azure/pubspec.yaml
  43. 14
      lib/core/bindings/initial_binding.dart
  44. 89
      lib/core/utils/jieli_constants.dart
  45. 2
      lib/core/widgets/message_bubble.dart
  46. 13
      lib/data/models/events/voice_interaction_event.dart
  47. 2
      lib/data/services/ai_service.dart
  48. 105
      lib/data/services/chat_history_service.dart
  49. 97
      lib/data/services/function_service.dart
  50. 278
      lib/data/services/open_ai_service_adapter.dart
  51. 4
      lib/data/services/speech_impl/azure_asr_service.dart
  52. 2
      lib/data/services/speech_impl/azure_tts_service.dart
  53. 15
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  54. 8
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  55. 85
      lib/data/services/voice_interaction_service.dart
  56. 315
      lib/modules/chat/controllers/chat_controller.dart
  57. 20
      lib/modules/chat/models/message_model.dart
  58. 47
      lib/modules/chat/views/chat_navigation_card.dart
  59. 2
      lib/modules/chat/views/chat_view.dart
  60. 11
      lib/modules/jieli_test/bindings/jieli_test_binding.dart
  61. 323
      lib/modules/jieli_test/controllers/jieli_test_controller.dart
  62. 568
      lib/modules/jieli_test/views/jieli_test_view.dart
  63. 20
      lib/modules/meeting/controllers/meeting_record_controller.dart
  64. 19
      lib/modules/meeting/views/meeting_details_view.dart
  65. 15
      lib/modules/meeting/views/meeting_edit_view.dart
  66. 15
      lib/modules/meeting/views/meeting_record_view.dart
  67. 4
      lib/modules/pairing/controllers/pairing_controller.dart
  68. 45
      lib/modules/profile/controllers/profile_controller.dart
  69. 22
      lib/modules/settings/views/settings_view.dart
  70. 7
      lib/modules/splash/controllers/splash_controller.dart
  71. 7
      lib/routes/app_pages.dart
  72. 1
      lib/routes/app_routes.dart
  73. 0
      local_plugins/azure_speech/LICENSE
  74. 136
      local_plugins/azure_speech/README.md
  75. 56
      local_plugins/azure_speech/android/build.gradle.kts
  76. 1
      local_plugins/azure_speech/android/settings.gradle.kts
  77. 6
      local_plugins/azure_speech/android/src/main/AndroidManifest.xml
  78. 593
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  79. 297
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  80. 12
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  81. 45
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/utils/FileLogger.kt
  82. 0
      local_plugins/azure_speech/ios/Classes/.gitkeep
  83. 461
      local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift
  84. 182
      local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift
  85. 330
      local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift
  86. 24
      local_plugins/azure_speech/ios/azure_speech.podspec
  87. 32
      local_plugins/azure_speech/pubspec.yaml
  88. 0
      local_plugins/chat_storage/LICENSE
  89. 80
      local_plugins/chat_storage/README.md
  90. 60
      local_plugins/chat_storage/android/build.gradle.kts
  91. 1
      local_plugins/chat_storage/android/settings.gradle.kts
  92. 8
      local_plugins/chat_storage/android/src/main/AndroidManifest.xml
  93. 236
      local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt
  94. 0
      local_plugins/chat_storage/ios/Classes/.gitkeep
  95. 23
      local_plugins/chat_storage/ios/chat_storage.podspec
  96. 154
      local_plugins/chat_storage/lib/chat_storage.dart
  97. 30
      local_plugins/chat_storage/pubspec.yaml
  98. 144
      local_plugins/jieli/README.md
  99. 55
      local_plugins/jieli/android/build.gradle.kts
  100. 1
      local_plugins/jieli/android/settings.gradle.kts

79
.cursorrules

@ -1,10 +1,73 @@
1. 不要主动运行程序, 如flutter run
2. 不要改动android, iOS项目的配置信息,包括SDK, NDK版本等
3. 不要使用print, 而是使用logger
4. 尽可能使用GetX提供的各种功能, 而不是使用原生的StatefulWidget
5. 不要增加重试逻辑,出现失败直接返回错误
6. 尽可能避免使用try catch
7. 尽可能避免打印日志,仅在出错时记录日志
1. 请严格遵循 Effective Dart 的风格指南来编写代码
2. 不要主动运行程序, 如flutter run
3. 不要改动android, iOS项目的配置信息,包括SDK, NDK, JAVA版本等
4. 不要使用print, 而是使用logger, 尽可能避免打印日志,仅在出错时记录日志
5. 尽可能使用GetX提供的各种功能, 而不是使用原生的StatefulWidget
6. 不要增加重试逻辑,出现失败直接返回错误
7. 尽可能避免使用try catch
8. 请使用较小的代码量改动,最安全的方法进行代码编写和优化
9. 请尽可能使用中文回复问题
10. 重要:不要过度设计!!
10. 重要:不要过度设计!!
11. 本项目使用 Kotlin DSL 进行开发
# 项目架构概述
DeepSound是一个基于Flutter和GetX构建的多平台语音应用,具有语音合成、语音识别、语音聊天等功能。
## 总体架构
- 采用GetX状态管理框架
- 遵循模块化设计,各功能模块相对独立
- 使用依赖注入实现组件解耦
- 多语言支持,使用GetX国际化方案
## 主要目录结构
- lib/core: 核心组件、工具和通用功能
- lib/data: 数据模型、服务和提供者
- lib/modules: 功能模块,按业务划分
- lib/routes: 路由定义和管理
- local_plugins: 本地插件,提供原生功能支持
# 核心模块说明
## 语音处理
- 火山语音服务: 提供语音识别和合成核心能力
- Azure语音服务: 提供备选的语音识别和合成能力
- 离线TTS支持: 在线语音合成失败时的备选方案
## 聊天功能
- 基于OpenAI的对话功能
- 支持语音输入、文字输入和语音输出
- 支持多种角色和语音类型
## 用户系统
- 支持多种登录方式:Firebase、Google、Facebook
- 用户资料管理和设置
- 主题切换和语言设置
## 蓝牙配对
- 支持蓝牙设备发现和连接
- 提供简化的配对流程
- 设备管理和状态监控
## 音频处理
- 音频录制和播放
- 支持波形显示
- 音量控制和音频质量设置
# 技术栈
- Flutter: UI框架
- GetX: 状态管理和依赖注入
- Firebase: 用户认证和后端服务
- WebSocket: 实时通信
- SQLite: 本地数据存储
- 原生插件: 语音识别、合成和蓝牙功能
# 插件说明
- jieli: 杰理蓝牙设备连接和控制插件
- volcano_speech: 火山语音服务插件,提供语音识别和合成
- azure_speech: 微软Azure语音服务插件
- open_ai_service: OpenAI服务插件,提供AI对话能力
- speech_to_text: 通用语音识别插件
# 环境配置
- 环境变量通过.env文件管理
- 支持不同环境的配置切换
- 敏感信息不应直接硬编码在代码中

12
.mcp.json

@ -0,0 +1,12 @@
{
"mcpServers": {
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=e5fdc9605eabdeb5626f18f5721f343d"
},
"web-search": {
"url": "http://mcp.ideapsound.com:8000/sse"
}
}
}

33
amap_flutter/.gitignore

@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/

33
amap_flutter/.metadata

@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: android
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
amap_flutter/CHANGELOG.md

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
amap_flutter/LICENSE

@ -0,0 +1 @@
TODO: Add your license here.

15
amap_flutter/README.md

@ -0,0 +1,15 @@
# amap_flutter
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

4
amap_flutter/analysis_options.yaml

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

9
amap_flutter/android/.gitignore

@ -0,0 +1,9 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

54
amap_flutter/android/build.gradle

@ -0,0 +1,54 @@
group = "com.example.amap_flutter"
version = "1.0"
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.7.0")
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: "com.android.library"
android {
namespace = "com.example.amap_flutter"
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
defaultConfig {
minSdk = 21
}
dependencies {
implementation 'com.amap.api:3dmap-location-search:latest.integration'
testImplementation("junit:junit:4.13.2")
testImplementation("org.mockito:mockito-core:5.0.0")
}
testOptions {
unitTests.all {
testLogging {
events "passed", "skipped", "failed", "standardOut", "standardError"
outputs.upToDateWhen {false}
showStandardStreams = true
}
}
}
}

1
amap_flutter/android/settings.gradle

@ -0,0 +1 @@
rootProject.name = 'amap_flutter'

3
amap_flutter/android/src/main/AndroidManifest.xml

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.amap_flutter">
</manifest>

168
amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java

@ -0,0 +1,168 @@
package com.example.amap_flutter;
import android.content.Context;
import androidx.annotation.NonNull;
import android.util.Log;
import com.amap.api.location.AMapLocation;
import com.amap.api.location.AMapLocationClient;
import com.amap.api.location.AMapLocationClientOption;
import com.amap.api.location.AMapLocationListener;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.EventChannel;
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;
import java.util.HashMap;
import java.util.Map;
/** AmapFlutterPlugin */
public class AmapFlutterPlugin implements FlutterPlugin, MethodCallHandler {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
/// when the Flutter Engine is detached from the Activity
private MethodChannel methodChannel;
private EventChannel eventChannel;
private Context applicationContext;
private AMapLocationClient mLocationClient;
private EventChannel.EventSink eventSink;
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
applicationContext = flutterPluginBinding.getApplicationContext();
methodChannel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "amap_flutter");
methodChannel.setMethodCallHandler(this);
// 初始化EventChannel
eventChannel = new EventChannel(flutterPluginBinding.getBinaryMessenger(), "amap_flutter/events");
eventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
eventSink = events;
}
@Override
public void onCancel(Object arguments) {
eventSink = null;
}
});
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
switch (call.method) {
case "setApiKey":
setApiKey(call, result);
break;
case "initLocation":
initLocation(result);
break;
case "startLocation":
startLocation(result);
break;
case "stopLocation":
stopLocation(result);
break;
case "dispose":
dispose(result);
break;
default:
result.notImplemented();
}
}
private void setApiKey(MethodCall call, Result result) {
try {
String apiKey = call.argument("androidKey");
AMapLocationClient.updatePrivacyShow(applicationContext, true, true);
AMapLocationClient.updatePrivacyAgree(applicationContext, true);
result.success(null);
} catch (Exception e) {
result.error("INIT_ERROR", "初始化失败", e.getMessage());
}
}
private void initLocation(Result result) {
try {
mLocationClient = new AMapLocationClient(applicationContext);
// 配置定位参数
AMapLocationClientOption option = new AMapLocationClientOption();
option.setOnceLocation(true);
option.setNeedAddress(true);
mLocationClient.setLocationOption(option);
mLocationClient.setLocationListener(mLocationListener);
result.success(null);
} catch (Exception e) {
result.error("INIT_ERROR", "初始化失败", e.getMessage());
}
}
private void startLocation(Result result) {
mLocationClient.startLocation();
result.success(null);
}
private void stopLocation(Result result) {
destroyLocationClient();
result.success(null);
}
private void dispose(Result result) {
try {
destroyLocationClient();
result.success(null);
} catch (Exception e) {
result.error("DISPOSE_ERROR", "资源释放失败", e.getMessage());
}
}
private void destroyLocationClient() {
if (mLocationClient != null) {
try {
// 停止定位
mLocationClient.stopLocation();
// 销毁定位客户端
mLocationClient.onDestroy();
mLocationClient = null;
// 关闭事件通道
if (eventSink != null) {
eventSink.endOfStream();
eventSink = null;
}
} catch (Exception e) {
Log.e("AmapFlutter", "销毁定位客户端失败", e);
}
}
}
AMapLocationListener mLocationListener = new AMapLocationListener() {
@Override
public void onLocationChanged(AMapLocation amapLocation) {
if (eventSink == null) return;
if (amapLocation != null) {
if (amapLocation.getErrorCode() == 0) {
Map<String, Object> locationData = new HashMap<>();
locationData.put("latitude", amapLocation.getLatitude());
locationData.put("longitude", amapLocation.getLongitude());
locationData.put("address", amapLocation.getAddress());
eventSink.success(locationData);
}
}
}
};
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
methodChannel.setMethodCallHandler(null);
eventChannel.setStreamHandler(null);
destroyLocationClient();
}
}

38
amap_flutter/ios/.gitignore

@ -0,0 +1,38 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

0
amap_flutter/ios/Assets/.gitkeep

19
amap_flutter/ios/Classes/AmapFlutterPlugin.swift

@ -0,0 +1,19 @@
import Flutter
import UIKit
public class AmapFlutterPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "amap_flutter", binaryMessenger: registrar.messenger())
let instance = AmapFlutterPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}

14
amap_flutter/ios/Resources/PrivacyInfo.xcprivacy

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

29
amap_flutter/ios/amap_flutter.podspec

@ -0,0 +1,29 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint amap_flutter.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'amap_flutter'
s.version = '0.0.1'
s.summary = 'A new Flutter project.'
s.description = <<-DESC
A new Flutter project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'amap_flutter_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
end

36
amap_flutter/lib/amap_flutter.dart

@ -0,0 +1,36 @@
import 'amap_flutter_platform_interface.dart';
class AmapFlutter {
// 初始化SDK
Future<void> initSdk({String? iosKey, String? androidKey}) async {
await AmapFlutterPlatform.instance.setApiKey(
iosKey: iosKey,
androidKey: androidKey,
);
}
// 定位初始化
Future<void> initLocation() async {
return AmapFlutterPlatform.instance.initLocation();
}
// 开始定位
Future<void> startLocation() async {
return AmapFlutterPlatform.instance.startLocation();
}
// 监听定位返回位置
Stream<dynamic> locationStream() {
return AmapFlutterPlatform.instance.locationStream();
}
// 停止定位
Future<void> stopLocation() async {
await AmapFlutterPlatform.instance.stopLocation();
}
// 销毁
Future<void> dispose() async {
await AmapFlutterPlatform.instance.dispose();
}
}

45
amap_flutter/lib/amap_flutter_method_channel.dart

@ -0,0 +1,45 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'amap_flutter_platform_interface.dart';
/// An implementation of [AmapFlutterPlatform] that uses method channels.
class MethodChannelAmapFlutter extends AmapFlutterPlatform {
/// The method channel used to interact with the native platform.
@visibleForTesting
final methodChannel = const MethodChannel('amap_flutter');
final eventChannel = const EventChannel('amap_flutter/events');
@override
Future<void> setApiKey({String? iosKey, String? androidKey}) async {
await methodChannel.invokeMethod('setApiKey', {
"iosKey": iosKey,
"androidKey": androidKey,
});
}
@override
Future<void> initLocation() async {
return await methodChannel.invokeMethod('initLocation');
}
@override
Future<void> startLocation() async {
return await methodChannel.invokeMethod('startLocation');
}
@override
Stream<dynamic> locationStream() {
return eventChannel.receiveBroadcastStream();
}
@override
Future<void> stopLocation() async {
await methodChannel.invokeMethod('stopLocation');
}
@override
Future<void> dispose() async {
await methodChannel.invokeMethod('dispose');
}
}

49
amap_flutter/lib/amap_flutter_platform_interface.dart

@ -0,0 +1,49 @@
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'amap_flutter_method_channel.dart';
abstract class AmapFlutterPlatform extends PlatformInterface {
/// Constructs a AmapFlutterPlatform.
AmapFlutterPlatform() : super(token: _token);
static final Object _token = Object();
static AmapFlutterPlatform _instance = MethodChannelAmapFlutter();
/// The default instance of [AmapFlutterPlatform] to use.
///
/// Defaults to [MethodChannelAmapFlutter].
static AmapFlutterPlatform get instance => _instance;
/// Platform-specific implementations should set this with their own
/// platform-specific class that extends [AmapFlutterPlatform] when
/// they register themselves.
static set instance(AmapFlutterPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<void> setApiKey({String? iosKey, String? androidKey}) {
throw UnimplementedError('init() has not been implemented.');
}
Future<void> initLocation() {
throw UnimplementedError('initLocation() has not been implemented.');
}
Future<void> startLocation() {
throw UnimplementedError('startLocation() has not been implemented.');
}
Stream<dynamic> locationStream() {
throw UnimplementedError('locationStream() has not been implemented.');
}
Future<void> stopLocation() {
throw UnimplementedError('stopLocation() has not been implemented.');
}
Future<void> dispose() {
throw UnimplementedError('dispose() has not been implemented.');
}
}

72
amap_flutter/pubspec.yaml

@ -0,0 +1,72 @@
name: amap_flutter
description: "A new Flutter project."
version: 0.0.1
homepage:
environment:
sdk: ^3.7.0
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.example.amap_flutter
pluginClass: AmapFlutterPlugin
ios:
pluginClass: AmapFlutterPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

45
android/app/build.gradle.kts

@ -3,6 +3,9 @@ import java.io.FileInputStream
repositories {
google()
mavenCentral()
flatDir {
dirs(project(":spotify_sdk").projectDir.resolve("libs"))
}
}
plugins {
@ -34,7 +37,32 @@ android {
jvmTarget = JavaVersion.VERSION_11.toString()
}
// 添加lint选项
lintOptions {
isCheckReleaseBuilds = false
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
defaultConfig {
manifestPlaceholders.putAll(
mapOf(
"redirectSchemeName" to "spotify-sdk",
"redirectHostName" to "auth"
)
)
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.yunqiinnovation.deepsound"
// You can update the following values to match your application needs.
@ -82,14 +110,22 @@ android {
dependencies {
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
// 添加本地插件模块依赖
implementation(project(":azure_speech"))
implementation(project(":open_ai_service"))
implementation(project(":volcano_speech"))
implementation(project(":chat_storage")) // 添加新创建的SQLite聊天存储插件
implementation(project(":jieli")) // 添加新创建的SQLite聊天存储插件
implementation(project(":spotify_sdk"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// Microsoft 语音识别SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.42.0")
// 添加androidx.media依赖
implementation("androidx.media:media:1.6.0")
@ -100,11 +136,6 @@ dependencies {
// 添加 AndroidX Security 加密 SharedPreferences 依赖
implementation("androidx.security:security-crypto:1.1.0-alpha06")
// // 添加火山语音合成SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8")
// 添加火山语音识别SDK依赖
// implementation("com.bytedance.speechengine:speechengine_tob:0.0.5")
}
flutter {

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

@ -27,9 +27,30 @@
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
<!-- 添加悬浮窗权限,可能有助于解决前台服务启动问题 -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- 添加发送短信权限 -->
<uses-permission android:name="android.permission.SEND_SMS" />
<!-- 添加读取联系人权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- 添加拨打电话权限 -->
<uses-permission android:name="android.permission.CALL_PHONE" />
<!-- 添加查询应用包权限 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<!--允许获取网络状态,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--允许获取wifi网络信息,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--允许获取wifi状态改变,用于网络定位(无gps情况下的定位),若需网络定位功能则必选-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<!--后台获取位置信息,若需后台定位则必选-->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!--用于申请调用A-GPS模块,卫星定位加速-->
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
<!--允许写设备缓存,用于问题排查-->
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<application
android:label="DeepSound"
android:label="DeapSound"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
@ -38,7 +59,7 @@
android:enableOnBackInvokedCallback="true"
tools:replace="android:label">
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/>
<!-- 添加语音交互服务 -->
@ -83,6 +104,10 @@
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="yqcxdeepsound"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
@ -90,6 +115,12 @@
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="e5fdc9605eabdeb5626f18f5721f343d"/>
<service android:name="com.amap.api.location.APSService"></service>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and

654
android/app/src/main/kotlin/com/example/deep_voice/AzureAsrHelper.kt

@ -1,654 +0,0 @@
package com.yunqiinnovation.deepsound
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.os.Process
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
// 移除WebRTC相关导入
class AzureAsrHelper(private val context: Context) {
private var recognizer: SpeechRecognizer? = null
private var speechConfig: SpeechConfig? = null
private val TAG = "AzureAsrHelper"
private var isContinuousRecognitionActive = false
private var currentLanguage = "zh-CN"
private var subscriptionKey = ""
private var serviceRegion = ""
private var isAutoDetectLanguage = false
private var supportedLanguages = arrayOf("zh-CN", "en-US")
// 是否使用回音消除 - 内部控制常量
private val useEchoCancellation = true
// 自定义音频处理相关
private var customAudioProcessor: CustomAudioProcessor? = null
private var pushStream: PushAudioInputStream? = null
private var audioConfig: AudioConfig? = null
// 初始化SDK并创建recognizer
fun initialize(subscriptionKey: String, serviceRegion: String,
supportedLanguages: Array<String> = arrayOf("zh-CN", "en-US")): Boolean {
try {
FileLogger.d(TAG, "初始化 Azure 语音服务")
// 检查配置是否为空
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty()) {
FileLogger.e(TAG, "Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
this.subscriptionKey = subscriptionKey
this.serviceRegion = serviceRegion
// 设置语言
if (supportedLanguages.isNotEmpty()) {
this.supportedLanguages = supportedLanguages
}
// 根据支持的语言数量决定是否启用自动语言检测
this.isAutoDetectLanguage = supportedLanguages.size >= 2
// 如果只有一种语言,设置为当前语言
if (!isAutoDetectLanguage && supportedLanguages.isNotEmpty()) {
this.currentLanguage = supportedLanguages[0]
}
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
// 设置语言配置
if (isAutoDetectLanguage) {
// 设置自动语言检测
speechConfig?.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous")
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
}
// 创建识别器
try {
if (useEchoCancellation) {
// 如果使用回音消除,创建自定义音频输入流
setupCustomAudioProcessing()
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} else {
// 使用默认麦克风输入
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
}
FileLogger.d(TAG, "Azure 语音服务初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "创建识别器失败: ${e.message}")
stopCustomAudioProcessing()
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}")
return false
}
}
// 重置 recognizer
private fun resetRecognizer(): Boolean {
try {
// 释放之前的 recognizer
recognizer?.close()
recognizer = null
// 停止当前的音频处理
stopCustomAudioProcessing()
// 使用现有配置重新创建 recognizer
if (speechConfig != null) {
if (useEchoCancellation) {
// 如果使用回音消除,创建自定义音频输入流
setupCustomAudioProcessing()
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} else {
// 使用默认麦克风输入
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
}
return true
} else {
FileLogger.e(TAG, "语音配置未初始化")
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "重置识别器失败: ${e.message}")
return false
}
}
// 开始一次性语音识别
fun recognizeOnce(callback: RecognizeCallback) {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return
}
// 重置 recognizer
if (!resetRecognizer()) {
callback.onError("重置识别器失败")
return
}
try {
// 启动音频处理
startCustomAudioProcessing()
// 执行识别
val result = recognizer?.recognizeOnceAsync()?.get()
// 停止音频处理
stopCustomAudioProcessing()
if (result != null && result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language
callback.onResult(result.text, detectedLanguage ?: "")
} else {
callback.onError("未能识别语音")
}
} catch (e: Exception) {
// 停止音频处理
stopCustomAudioProcessing()
callback.onError("识别异常: ${e.message}")
}
}
// 开始连续语音识别
fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return false
}
// 如果已经在进行连续识别,先停止
if (isContinuousRecognitionActive) {
stopContinuousRecognition(callback)
}
// 重置 recognizer
if (!resetRecognizer()) {
callback.onError("重置识别器失败")
return false
}
try {
// 启动音频处理
startCustomAudioProcessing()
// 设置识别事件处理
// 最终识别结果
recognizer?.recognized?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
} else {
currentLanguage
}
// FileLogger.d(TAG, "最终识别结果: ${event.result.text}")
callback.onResult(event.result.text, detectedLanguage)
}
}
)
// 识别中事件
recognizer?.recognizing?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizingSpeech) {
val detectedLanguage = if (isAutoDetectLanguage) {
AutoDetectSourceLanguageResult.fromResult(event.result)?.language ?: ""
} else {
currentLanguage
}
// FileLogger.d(TAG, "识别中结果: ${event.result.text}")
callback.onRecognizing(event.result.text, detectedLanguage)
}
}
)
// 会话事件
recognizer?.sessionStarted?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = true
callback.onSessionStarted()
}
)
recognizer?.sessionStopped?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
isContinuousRecognitionActive = false
callback.onSessionStopped()
}
)
// 取消事件
recognizer?.canceled?.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = if (event.reason == CancellationReason.Error) event.errorDetails else ""
callback.onCanceled(event.reason.toString(), errorDetails)
isContinuousRecognitionActive = false
}
)
// 开始连续识别
recognizer?.startContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = true
return true
} catch (e: Exception) {
callback.onError("开始连续识别失败: ${e.message}")
isContinuousRecognitionActive = false
stopCustomAudioProcessing()
return false
}
}
// 停止连续语音识别
fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (!isContinuousRecognitionActive || recognizer == null) {
return true
}
try {
recognizer?.stopContinuousRecognitionAsync()
isContinuousRecognitionActive = false
callback.onSessionStopped()
// 停止音频处理
stopCustomAudioProcessing()
return true
} catch (e: Exception) {
callback.onError("停止连续识别失败: ${e.message}")
isContinuousRecognitionActive = false
stopCustomAudioProcessing()
return false
}
}
// 检查连续识别是否活跃
fun isContinuousRecognitionActive(): Boolean {
return isContinuousRecognitionActive
}
// 设置自定义音频处理
private fun setupCustomAudioProcessing() {
if (!useEchoCancellation) {
return
}
try {
// 1. 创建PushAudioInputStream
pushStream = PushAudioInputStream.create()
// 2. 创建AudioConfig
audioConfig = AudioConfig.fromStreamInput(pushStream)
// 3. 创建自定义音频处理器
customAudioProcessor = CustomAudioProcessor(pushStream)
FileLogger.d(TAG, "自定义音频处理设置完成")
} catch (e: Exception) {
FileLogger.e(TAG, "设置自定义音频处理失败: ${e.message}")
releaseCustomAudioProcessing()
// 降级处理:如果自定义处理设置失败,尝试使用默认麦克风
try {
FileLogger.d(TAG, "尝试降级到默认麦克风输入")
audioConfig = AudioConfig.fromDefaultMicrophoneInput()
} catch (e2: Exception) {
FileLogger.e(TAG, "默认麦克风输入设置也失败: ${e2.message}")
audioConfig = null
}
}
}
// 启动自定义音频处理
private fun startCustomAudioProcessing() {
if (!useEchoCancellation || customAudioProcessor == null) {
return
}
try {
customAudioProcessor?.startRecording()
FileLogger.d(TAG, "自定义音频处理已启动")
} catch (e: Exception) {
FileLogger.e(TAG, "启动自定义音频处理失败: ${e.message}")
}
}
// 停止自定义音频处理
private fun stopCustomAudioProcessing() {
if (!useEchoCancellation || customAudioProcessor == null) {
return
}
try {
customAudioProcessor?.stopRecording()
FileLogger.d(TAG, "自定义音频处理已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止自定义音频处理失败: ${e.message}")
}
}
// 释放自定义音频处理资源
private fun releaseCustomAudioProcessing() {
stopCustomAudioProcessing()
try {
customAudioProcessor = null
pushStream?.close()
pushStream = null
audioConfig?.close()
audioConfig = null
FileLogger.d(TAG, "自定义音频处理资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放自定义音频处理资源时出错: ${e.message}")
}
}
// 释放所有资源
fun dispose() {
try {
// 停止和释放音频处理
releaseCustomAudioProcessing()
recognizer?.close()
recognizer = null
speechConfig?.close()
speechConfig = null
isContinuousRecognitionActive = false
FileLogger.d(TAG, "语音识别资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源出错: ${e.message}")
}
}
// 自定义音频处理器 - 使用Android原生回音消除
private inner class CustomAudioProcessor(private val pushStream: PushAudioInputStream?) {
private val SAMPLE_RATE = 16000
private val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO
private val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT
private val BUFFER_SIZE = SAMPLE_RATE * 2 // 简化缓冲区大小计算,更稳定
private var audioRecord: AudioRecord? = null
private var echoCanceler: AcousticEchoCanceler? = null
private val isRecording = AtomicBoolean(false)
private var recordingThread: Thread? = null
// 启动录音并处理音频数据
fun startRecording() {
if (isRecording.get() || pushStream == null) {
return
}
try {
// 使用Builder模式构建AudioFormat
val audioFormat = AudioFormat.Builder()
.setSampleRate(SAMPLE_RATE)
.setEncoding(AUDIO_FORMAT)
.setChannelMask(CHANNEL_CONFIG)
.build()
// 使用Builder模式创建AudioRecord实例
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(BUFFER_SIZE)
.build()
// 检查AudioRecord初始化状态
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
FileLogger.e(TAG, "AudioRecord初始化失败,状态: ${audioRecord?.state}")
// 尝试使用DEFAULT音频源重试一次
audioRecord?.release()
audioRecord = AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.DEFAULT)
.setAudioFormat(audioFormat)
.setBufferSizeInBytes(BUFFER_SIZE)
.build()
if (audioRecord?.state != AudioRecord.STATE_INITIALIZED) {
FileLogger.e(TAG, "AudioRecord初始化第二次尝试也失败,放弃")
releaseAudioResources()
return
} else {
FileLogger.d(TAG, "使用默认音频源成功初始化AudioRecord")
}
}
// 启用音频效果(回音消除、噪声抑制等)
enableAudioEffects()
// 启动录音
audioRecord?.startRecording()
isRecording.set(true)
// 创建录音线程
recordingThread = Thread({
val buffer = ByteArray(BUFFER_SIZE)
while (isRecording.get()) {
try {
val readSize = audioRecord?.read(buffer, 0, BUFFER_SIZE) ?: 0
if (readSize > 0) {
try {
// 将处理后的音频数据推送到流
if (readSize == buffer.size) {
// 如果读取的大小等于buffer的大小,直接写入整个buffer
pushStream.write(buffer)
} else {
// 如果只读取了部分数据,创建新的数组只包含有效数据
val validData = buffer.copyOfRange(0, readSize)
pushStream.write(validData)
}
} catch (e: Exception) {
FileLogger.e(TAG, "写入音频数据失败: ${e.message}")
break
}
} else if (readSize == 0) {
// 读取为0,可能是临时的,等待一下继续尝试
Thread.sleep(10)
} else {
// 负值表示错误
FileLogger.e(TAG, "读取音频数据失败,错误码: $readSize")
break
}
} catch (e: Exception) {
FileLogger.e(TAG, "录音线程异常: ${e.message}")
break
}
}
}, "AudioRecordingThread")
// 设置线程优先级并启动
recordingThread?.priority = Thread.MAX_PRIORITY
recordingThread?.start()
FileLogger.d(TAG, "音频录制已启动" + (if(echoCanceler?.enabled == true) ",回音消除已启用" else ""))
} catch (e: Exception) {
FileLogger.e(TAG, "启动音频录制失败: ${e.message}")
releaseAudioResources()
}
}
// 启用音频效果(回音消除、噪声抑制等)
private fun enableAudioEffects() {
try {
val audioSessionId = audioRecord?.audioSessionId ?: -1
if (audioSessionId != -1) {
// 启用回音消除
if (AcousticEchoCanceler.isAvailable()) {
try {
echoCanceler = AcousticEchoCanceler.create(audioSessionId)
if (echoCanceler != null) {
echoCanceler?.enabled = true
FileLogger.d(TAG, "回音消除已启用,会话ID: $audioSessionId")
} else {
FileLogger.w(TAG, "回音消除器创建返回null")
}
} catch (e: Exception) {
FileLogger.e(TAG, "创建回音消除器时出错: ${e.message}")
}
} else {
FileLogger.d(TAG, "设备不支持回音消除")
}
// 以下功能暂时不启用,可根据需要取消注释
/*
// 启用噪声抑制
if (NoiseSuppressor.isAvailable()) {
try {
val ns = NoiseSuppressor.create(audioSessionId)
ns?.enabled = true
FileLogger.d(TAG, "噪声抑制已启用")
} catch (e: Exception) {
FileLogger.e(TAG, "创建噪声抑制器时出错: ${e.message}")
}
}
// 启用自动增益控制
if (AutomaticGainControl.isAvailable()) {
try {
val agc = AutomaticGainControl.create(audioSessionId)
agc?.enabled = true
FileLogger.d(TAG, "自动增益控制已启用")
} catch (e: Exception) {
FileLogger.e(TAG, "创建自动增益控制时出错: ${e.message}")
}
}
*/
} else {
FileLogger.w(TAG, "无效的音频会话ID,无法启用音频效果")
}
} catch (e: Exception) {
FileLogger.e(TAG, "启用音频效果时出错: ${e.message}")
}
}
// 停止录音
fun stopRecording() {
if (!isRecording.get()) {
return
}
isRecording.set(false)
try {
// 等待录音线程结束
recordingThread?.join(1000)
// 释放资源
releaseAudioResources()
FileLogger.d(TAG, "音频录制已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止音频录制失败: ${e.message}")
}
}
// 释放音频资源
private fun releaseAudioResources() {
try {
// 停止录音
try {
if (audioRecord?.state == AudioRecord.STATE_INITIALIZED) {
audioRecord?.stop()
}
} catch (e: Exception) {
// 忽略可能的IllegalStateException
FileLogger.w(TAG, "停止AudioRecord时出错: ${e.message}")
}
// 释放回音消除器
try {
if (echoCanceler != null) {
echoCanceler?.enabled = false
echoCanceler?.release()
echoCanceler = null
}
} catch (e: Exception) {
FileLogger.w(TAG, "释放回音消除器时出错: ${e.message}")
} finally {
echoCanceler = null
}
// 释放音频记录器
try {
audioRecord?.release()
} catch (e: Exception) {
FileLogger.w(TAG, "释放AudioRecord时出错: ${e.message}")
} finally {
audioRecord = null
}
// 重置线程
recordingThread = null
} catch (e: Exception) {
FileLogger.e(TAG, "释放音频资源失败: ${e.message}")
}
}
}
// 一次性识别回调接口
interface RecognizeCallback {
fun onResult(result: String, detectedLanguage: String = "")
fun onError(error: String)
}
// 连续识别回调接口
interface ContinuousRecognizeCallback {
fun onResult(result: String, detectedLanguage: String = "")
fun onRecognizing(recognizing: String, detectedLanguage: String = "")
fun onSessionStarted()
fun onSessionStopped()
fun onCanceled(reason: String, errorDetails: String)
fun onError(error: String)
}
}

344
android/app/src/main/kotlin/com/example/deep_voice/VolcanoAIService.kt

@ -1,344 +0,0 @@
package com.yunqiinnovation.deepsound
import android.util.Log
import okhttp3.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONArray
import org.json.JSONObject
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* 火山AI服务的原生实现
*
* 参考Flutter端的VolcanoAIService实现,提供同步和异步的API调用方式
*/
class VolcanoAIService() {
private val TAG = "VolcanoAIService"
private val baseUrl = "https://ark.cn-beijing.volces.com/api/v3"
private val chatEndpoint = "/chat/completions"
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
private var apiKey: String = ""
private var isInitialized = false
/**
* 初始化火山AI服务
*
* @param apiKey 火山AI API密钥
* @return 初始化是否成功
*/
fun initialize(apiKey: String): Boolean {
this.apiKey = apiKey
isInitialized = apiKey.isNotEmpty()
if (!isInitialized) {
Log.e(TAG, "初始化失败:API key 不能为空")
} else {
Log.d(TAG, "火山AI服务初始化成功")
}
return isInitialized
}
/**
* 生成个性化问候语
*
* @param agentName 代理名称
* @param systemPrompt 系统提示词
* @param callback 回调函数,返回生成的问候语
*/
fun generateGreeting(agentName: String, systemPrompt: String, callback: (String?, Exception?) -> Unit) {
val messages = JSONArray().apply {
put(JSONObject().apply {
put("role", "system")
put("content", systemPrompt)
})
put(JSONObject().apply {
put("role", "user")
put("content", "请用一句简短的话向我打个招呼,要符合你的身份和性格特点,不要超过18个字。")
})
}
sendMessageStream(messages, systemPrompt, object : StreamCallback {
val stringBuilder = StringBuilder()
override fun onToken(token: String) {
stringBuilder.append(token)
}
override fun onComplete() {
callback(stringBuilder.toString(), null)
}
override fun onError(e: Exception) {
callback(null, e)
}
})
}
/**
* 发送消息(非流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回AI的回复
* @throws VolcanoAIException 如果API调用失败
*/
@Throws(VolcanoAIException::class)
fun sendMessage(messages: JSONArray, systemPrompt: String): String {
// 检查是否已初始化
if (!isInitialized || apiKey.isEmpty()) {
throw VolcanoAIException("火山AI服务未初始化或API key为空,请先调用initialize方法")
}
val fullMessages = JSONArray().apply {
put(JSONObject().apply {
put("role", "system")
put("content", systemPrompt)
})
for (i in 0 until messages.length()) {
put(messages.getJSONObject(i))
}
}
val requestBody = JSONObject().apply {
put("model", "doubao-1-5-lite-32k-250115")
put("messages", fullMessages)
put("temperature", 0.7)
put("max_tokens", 2000)
put("stream", false)
}
val mediaType = "application/json".toMediaTypeOrNull()
val request = Request.Builder()
.url("$baseUrl$chatEndpoint")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $apiKey")
.post(requestBody.toString().toRequestBody(mediaType))
.build()
try {
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
val errorBody = response.body?.string() ?: ""
val errorMessage = try {
JSONObject(errorBody).getJSONObject("error").getString("message")
} catch (e: Exception) {
"Unknown error occurred"
}
throw VolcanoAIException(errorMessage)
}
val responseBody = response.body?.string() ?: throw VolcanoAIException("Empty response")
val jsonResponse = JSONObject(responseBody)
if (jsonResponse.has("choices") &&
jsonResponse.getJSONArray("choices").length() > 0 &&
jsonResponse.getJSONArray("choices").getJSONObject(0).has("message")) {
return jsonResponse.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("message")
.getString("content")
}
throw VolcanoAIException("Invalid response format")
}
} catch (e: Exception) {
if (e is VolcanoAIException) throw e
throw VolcanoAIException("Failed to communicate with AI service: ${e.message}")
}
}
/**
* 发送消息(流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @param callback 回调函数,用于接收流式输出的结果
*/
fun sendMessageStream(messages: JSONArray, systemPrompt: String, callback: StreamCallback) {
// 检查是否已初始化
if (!isInitialized || apiKey.isEmpty()) {
callback.onError(VolcanoAIException("火山AI服务未初始化或API key为空,请先调用initialize方法"))
return
}
val fullMessages = JSONArray().apply {
put(JSONObject().apply {
put("role", "system")
put("content", systemPrompt)
})
for (i in 0 until messages.length()) {
put(messages.getJSONObject(i))
}
}
val requestBody = JSONObject().apply {
put("model", "doubao-1-5-lite-32k-250115")
put("messages", fullMessages)
put("temperature", 0.7)
put("max_tokens", 2000)
put("stream", true)
}
val mediaType = "application/json".toMediaTypeOrNull()
val request = Request.Builder()
.url("$baseUrl$chatEndpoint")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer $apiKey")
.addHeader("Accept", "text/event-stream")
.post(requestBody.toString().toRequestBody(mediaType))
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
callback.onError(VolcanoAIException("Failed to communicate with AI service: ${e.message}"))
}
override fun onResponse(call: Call, response: Response) {
if (!response.isSuccessful) {
val errorBody = response.body?.string() ?: ""
val errorMessage = try {
JSONObject(errorBody).getJSONObject("error").getString("message")
} catch (e: Exception) {
"Unknown error occurred"
}
callback.onError(VolcanoAIException(errorMessage))
return
}
val responseBody = response.body ?: return
val source = responseBody.source()
val bufferedSource = source.buffer
try {
while (!bufferedSource.exhausted()) {
val line = bufferedSource.readUtf8Line() ?: continue
if (line.isEmpty()) continue
if (line.startsWith("data: ")) {
val data = line.substring(6)
if (data == "[DONE]") {
callback.onComplete()
break
}
try {
val jsonData = JSONObject(data)
if (jsonData.has("choices") &&
jsonData.getJSONArray("choices").length() > 0 &&
jsonData.getJSONArray("choices").getJSONObject(0).has("delta") &&
jsonData.getJSONArray("choices").getJSONObject(0).getJSONObject("delta").has("content")) {
val content = jsonData.getJSONArray("choices")
.getJSONObject(0)
.getJSONObject("delta")
.getString("content")
callback.onToken(content)
}
} catch (e: Exception) {
// 忽略无效的JSON数据
continue
}
}
}
} catch (e: Exception) {
callback.onError(VolcanoAIException("Error processing stream: ${e.message}"))
} finally {
response.close()
}
}
})
}
/**
* 同步方式发送消息(流式输出)
*
* 注意:此方法会阻塞当前线程,请在后台线程中调用
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回完整的AI回复
* @throws VolcanoAIException 如果API调用失败
*/
@Throws(VolcanoAIException::class)
fun sendMessageStreamSync(messages: JSONArray, systemPrompt: String): String {
val result = StringBuilder()
val latch = CountDownLatch(1)
var exception: Exception? = null
sendMessageStream(messages, systemPrompt, object : StreamCallback {
override fun onToken(token: String) {
result.append(token)
}
override fun onComplete() {
latch.countDown()
}
override fun onError(e: Exception) {
exception = e
latch.countDown()
}
})
// 等待流式输出完成或出错
latch.await(60, TimeUnit.SECONDS)
if (exception != null) {
throw exception as VolcanoAIException
}
return result.toString()
}
/**
* 创建用户消息
*/
fun createUserMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "user")
put("content", content)
}
}
/**
* 创建系统消息
*/
fun createSystemMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "system")
put("content", content)
}
}
/**
* 创建助手消息
*/
fun createAssistantMessage(content: String): JSONObject {
return JSONObject().apply {
put("role", "assistant")
put("content", content)
}
}
/**
* 流式输出回调接口
*/
interface StreamCallback {
fun onToken(token: String)
fun onComplete()
fun onError(e: Exception)
}
}
/**
* 火山AI异常
*/
class VolcanoAIException(message: String) : Exception(message)

0
android/app/src/main/kotlin/com/example/deep_voice/ClassicBluetoothHelper.kt → android/app/src/main/kotlin/com/yunqiinnovation/deepsound/ClassicBluetoothHelper.kt

382
android/app/src/main/kotlin/com/example/deep_voice/MainActivity.kt → android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -1,5 +1,6 @@
package com.yunqiinnovation.deepsound
import android.Manifest
import android.content.Intent
import android.os.Build
import android.os.Bundle
@ -20,33 +21,54 @@ import android.bluetooth.BluetoothDevice
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
class MainActivity: FlutterActivity() {
private val AZURE_ASR_CHANNEL = "com.deep_voice.azure_asr"
private val AZURE_ASR_EVENT_CHANNEL = "com.deep_voice.azure_asr_events"
private val AZURE_TTS_CHANNEL = "com.deep_voice.azure_tts"
private val VOICE_INTERACTION_CHANNEL = "com.deep_voice.voice_interaction"
private val VOICE_INTERACTION_EVENT_CHANNEL = "com.deep_voice.voice_interaction_events"
private val CLASSIC_BLUETOOTH_CHANNEL = "com.deep_voice.classic_bluetooth"
private val CLASSIC_BLUETOOTH_EVENT_CHANNEL = "com.deep_voice.classic_bluetooth_events"
private val TAG = "MainActivity"
private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper
private lateinit var classicBluetoothHelper: ClassicBluetoothHelper
private var azureAsrEventSink: EventChannel.EventSink? = null
private var voiceInteractionEventSink: EventChannel.EventSink? = null
private var bluetoothEventSink: EventChannel.EventSink? = null
// 添加权限请求相关常量
private val PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
arrayOf(
Manifest.permission.RECORD_AUDIO,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.SEND_SMS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.CALL_PHONE,
Manifest.permission.POST_NOTIFICATIONS
)
} else {
arrayOf(
Manifest.permission.RECORD_AUDIO,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.SEND_SMS,
Manifest.permission.READ_CONTACTS,
Manifest.permission.CALL_PHONE
)
}
// 广播接收器
private val voiceInteractionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.d(TAG, "收到广播: ${intent?.action}")
FileLogger.d(TAG, "收到广播: ${intent?.action}")
when (intent?.action) {
VoiceInteractionService.ACTION_RECOGNITION_STARTED -> {
val timestamp = intent.getLongExtra("timestamp", 0)
Log.d(TAG, "收到语音识别启动广播: timestamp=$timestamp")
sendVoiceInteractionEvent(mapOf(
"type" to "recognition_started",
"timestamp" to timestamp
@ -58,12 +80,14 @@ class MainActivity: FlutterActivity() {
val assistantMessage = intent.getStringExtra("assistantMessage") ?: ""
val timestamp = intent.getLongExtra("timestamp", System.currentTimeMillis())
Log.d(TAG, "收到聊天记录更新广播: agentId=$agentId, timestamp=$timestamp")
Log.d(TAG, "用户消息: ${userMessage.take(50)}...")
Log.d(TAG, "助手回复: ${assistantMessage.take(50)}...")
sendChatHistoryEvent(agentId, userMessage, assistantMessage, timestamp)
}
VoiceInteractionService.ACTION_ENTER_TRANSLATION_MODE -> {
sendVoiceInteractionEvent(mapOf(
"type" to "enter_translation_mode",
"timestamp" to System.currentTimeMillis()
))
}
}
}
}
@ -143,13 +167,26 @@ class MainActivity: FlutterActivity() {
var azureSpeechKey: String = ""
var azureSpeechRegion: String = ""
var volcanoAiApiKey: String = ""
var openaiApiKey: String = ""
var openaiBaseUrl: String? = null
var openaiModel: String? = null
var volcanoSpeechAppId: String = ""
var volcanoSpeechAppToken: String = ""
var mcpServer: String = ""
// 安全存储相关常量
private const val SECURE_PREFS_FILENAME = "deep_voice_secure_prefs"
private const val KEY_AZURE_SPEECH_KEY = "azure_speech_key"
private const val KEY_AZURE_SPEECH_REGION = "azure_speech_region"
private const val KEY_VOLCANO_AI_API_KEY = "volcano_ai_api_key"
private const val KEY_OPENAI_API_KEY = "openai_api_key"
private const val KEY_OPENAI_BASE_URL = "openai_base_url"
private const val KEY_OPENAI_MODEL = "openai_model"
private const val KEY_MCP_SERVER_ENDPOINT = "mcp_server_endpoint"
private const val KEY_VOLCANO_SPEECH_APP_ID = "volcano_speech_app_id"
private const val KEY_VOLCANO_SPEECH_APP_TOKEN = "volcano_speech_app_token"
private const val KEY_MCP_SERVER = "mcp_server"
// 会话管理
private const val KEY_SESSION_ID = "session_id"
private var currentSessionId = ""
@ -183,7 +220,13 @@ class MainActivity: FlutterActivity() {
.putString(KEY_AZURE_SPEECH_KEY, azureSpeechKey)
.putString(KEY_AZURE_SPEECH_REGION, azureSpeechRegion)
.putString(KEY_VOLCANO_AI_API_KEY, volcanoAiApiKey)
.putString(KEY_OPENAI_API_KEY, openaiApiKey)
.putString(KEY_OPENAI_BASE_URL, openaiBaseUrl)
.putString(KEY_SESSION_ID, currentSessionId)
.putString(KEY_OPENAI_MODEL, openaiModel)
.putString(KEY_VOLCANO_SPEECH_APP_ID, volcanoSpeechAppId)
.putString(KEY_VOLCANO_SPEECH_APP_TOKEN, volcanoSpeechAppToken)
.putString(KEY_MCP_SERVER, mcpServer)
.apply()
FileLogger.d("MainActivity", "密钥已安全保存到加密存储中")
@ -227,11 +270,17 @@ class MainActivity: FlutterActivity() {
azureSpeechKey = sharedPreferences.getString(KEY_AZURE_SPEECH_KEY, "") ?: ""
azureSpeechRegion = sharedPreferences.getString(KEY_AZURE_SPEECH_REGION, "") ?: ""
volcanoAiApiKey = sharedPreferences.getString(KEY_VOLCANO_AI_API_KEY, "") ?: ""
openaiApiKey = sharedPreferences.getString(KEY_OPENAI_API_KEY, "") ?: ""
openaiBaseUrl = sharedPreferences.getString(KEY_OPENAI_BASE_URL, null)
openaiModel = sharedPreferences.getString(KEY_OPENAI_MODEL, null)
volcanoSpeechAppId = sharedPreferences.getString(KEY_VOLCANO_SPEECH_APP_ID, "") ?: ""
volcanoSpeechAppToken = sharedPreferences.getString(KEY_VOLCANO_SPEECH_APP_TOKEN, "") ?: ""
mcpServer = sharedPreferences.getString(KEY_MCP_SERVER, "") ?: ""
FileLogger.d("MainActivity", "已从加密存储加载密钥")
// 检查是否成功获取所有密钥
return azureSpeechKey.isNotEmpty() && azureSpeechRegion.isNotEmpty() && volcanoAiApiKey.isNotEmpty()
// 检查是否成功获取所有必要密钥
return azureSpeechKey.isNotEmpty() && azureSpeechRegion.isNotEmpty() &&
(openaiApiKey.isNotEmpty() || volcanoAiApiKey.isNotEmpty())
} catch (e: Exception) {
FileLogger.e("MainActivity", "从加密存储加载密钥失败: ${e.message}")
e.printStackTrace()
@ -246,9 +295,10 @@ class MainActivity: FlutterActivity() {
// 初始化 FileLogger
FileLogger.init(applicationContext)
// 请求必要权限
requestRequiredPermissions()
// 初始化 Azure 语音服务
azureAsrHelper = AzureAsrHelper(applicationContext)
azureTtsHelper = AzureTtsHelper(applicationContext)
classicBluetoothHelper = ClassicBluetoothHelper(applicationContext)
// 注册广播接收器
@ -266,7 +316,14 @@ class MainActivity: FlutterActivity() {
val voiceInteractionFilter = IntentFilter().apply {
addAction(VoiceInteractionService.ACTION_RECOGNITION_STARTED)
addAction(VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED)
addAction(VoiceInteractionService.ACTION_ENTER_TRANSLATION_MODE)
}
// 打印已注册的广播
FileLogger.d(TAG, "已注册语音交互广播:${VoiceInteractionService.ACTION_RECOGNITION_STARTED}, " +
"${VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED}, " +
"${VoiceInteractionService.ACTION_ENTER_TRANSLATION_MODE}")
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
registerReceiver(voiceInteractionReceiver, voiceInteractionFilter, Context.RECEIVER_NOT_EXPORTED)
} else {
@ -300,8 +357,7 @@ class MainActivity: FlutterActivity() {
setupMethodChannels(flutterEngine)
// 初始化 Azure 语音服务
azureAsrHelper = AzureAsrHelper(this)
azureTtsHelper = AzureTtsHelper(this)
classicBluetoothHelper = ClassicBluetoothHelper(this)
Log.d(TAG, "Flutter 引擎配置完成")
}
@ -327,20 +383,6 @@ class MainActivity: FlutterActivity() {
}
)
// Azure ASR 事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
Log.d(TAG, "ASR事件通道开始监听")
azureAsrEventSink = events
}
override fun onCancel(arguments: Any?) {
azureAsrEventSink = null
}
}
)
// 设置蓝牙事件通道
EventChannel(flutterEngine.dartExecutor.binaryMessenger, CLASSIC_BLUETOOTH_EVENT_CHANNEL).setStreamHandler(
object : EventChannel.StreamHandler {
@ -367,195 +409,6 @@ class MainActivity: FlutterActivity() {
private fun setupMethodChannels(flutterEngine: FlutterEngine) {
Log.d(TAG, "开始设置方法通道")
// 设置 Azure ASR 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_ASR_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey")
val region = call.argument<String>("region")
val supportedLanguages = call.argument<List<String>>("supportedLanguages")?.toTypedArray() ?: arrayOf("zh-CN", "en-US")
if (subscriptionKey == null || region == null) {
result.error("INVALID_ARGUMENTS", "订阅密钥和区域不能为空", null)
return@setMethodCallHandler
}
try {
val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages)
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
}
"recognizeOnce" -> {
azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
result.success(mapOf(
"text" to text,
"detectedLanguage" to detectedLanguage
))
}
override fun onError(error: String) {
result.error("RECOGNITION_ERROR", error, null)
}
})
}
"startContinuousRecognition" -> {
// 确保事件通道已准备好
if (azureAsrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
return@setMethodCallHandler
}
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
"type" to "result",
"text" to text,
"detectedLanguage" to detectedLanguage
))
}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
"type" to "recognizing",
"text" to recognizing,
"detectedLanguage" to detectedLanguage
))
}
override fun onSessionStarted() {
sendAsrEvent(mapOf("type" to "sessionStarted"))
}
override fun onSessionStopped() {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onCanceled(reason: String, errorDetails: String) {
sendAsrEvent(mapOf(
"type" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
))
}
override fun onError(error: String) {
sendAsrEvent(mapOf("type" to "error", "message" to error))
}
})
result.success(success)
}
"stopContinuousRecognition" -> {
try {
if (!azureAsrHelper.isContinuousRecognitionActive()) {
result.success(true)
return@setMethodCallHandler
}
val success = azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {}
override fun onCanceled(reason: String, errorDetails: String) {}
override fun onError(error: String) {
result.error("STOP_ERROR", error, null)
}
})
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", e.message, null)
}
}
"isContinuousRecognitionActive" -> {
result.success(azureAsrHelper.isContinuousRecognitionActive())
}
"dispose" -> {
azureAsrHelper.dispose()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
// 设置 Azure TTS 方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, AZURE_TTS_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val language = call.argument<String>("language") ?: "zh-CN"
val success = azureTtsHelper.initialize(subscriptionKey, region, language)
result.success(success)
}
"setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
result.success(azureTtsHelper.setVoice(voiceName))
}
"setSpeechParams" -> {
val rate = call.argument<Int>("rate") ?: 0
val pitch = call.argument<Int>("pitch") ?: 0
val volume = call.argument<Int>("volume") ?: 100
result.success(azureTtsHelper.setSpeechParams(rate, pitch, volume))
}
"setAudioOutputType" -> {
val outputTypeStr = call.argument<String>("outputType") ?: "speaker"
val outputType = when (outputTypeStr.lowercase()) {
"speaker" -> AzureTtsHelper.AudioOutputType.SPEAKER
"earpiece" -> AzureTtsHelper.AudioOutputType.EARPIECE
"auto" -> AzureTtsHelper.AudioOutputType.AUTO
else -> AzureTtsHelper.AudioOutputType.SPEAKER
}
result.success(azureTtsHelper.setAudioOutputType(outputType))
}
"speakText" -> {
val text = call.argument<String>("text") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "文本不能为空", null)
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
runOnUiThread { result.success(message) }
}
override fun onError(error: String) {
runOnUiThread { result.error("SPEAK_ERROR", error, null) }
}
})
}
"speakSsml" -> {
val ssml = call.argument<String>("ssml") ?: return@setMethodCallHandler result.error("INVALID_ARGUMENTS", "SSML不能为空", null)
azureTtsHelper.speakSsml(ssml, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
runOnUiThread { result.success(message) }
}
override fun onError(error: String) {
runOnUiThread { result.error("SPEAK_ERROR", error, null) }
}
})
}
"stopSpeaking" -> {
result.success(azureTtsHelper.stopSpeaking())
}
"isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking())
}
"dispose" -> {
azureTtsHelper.dispose()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
// 设置语音交互方法通道
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, VOICE_INTERACTION_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
@ -657,15 +510,6 @@ class MainActivity: FlutterActivity() {
}
}
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
if (azureAsrEventSink == null) return
runOnUiThread {
azureAsrEventSink?.success(event)
}
}
/**
* 启动语音交互服务(带配置参数)
*/
@ -673,14 +517,14 @@ class MainActivity: FlutterActivity() {
FileLogger.d(TAG, "启动语音交互服务")
// 获取配置参数
val key = call.argument<String>("azure_speech_key") ?: ""
val region = call.argument<String>("azure_speech_region") ?: ""
val aiKey = call.argument<String>("volcano_ai_api_key") ?: ""
// 设置 Azure Speech 配置
azureSpeechKey = key
azureSpeechRegion = region
volcanoAiApiKey = aiKey
azureSpeechKey = call.argument<String>("azure_speech_key") ?: ""
azureSpeechRegion = call.argument<String>("azure_speech_region") ?: ""
openaiApiKey = call.argument<String>("openai_api_key") ?: ""
openaiBaseUrl = call.argument<String>("openai_base_url")
openaiModel = call.argument<String>("openai_model")
volcanoSpeechAppId = call.argument<String>("volcano_speech_app_id") ?: ""
volcanoSpeechAppToken = call.argument<String>("volcano_speech_app_token") ?: ""
mcpServer = call.argument<String>("mcp_config") ?: ""
// 保存密钥到安全存储
saveKeysToSecureStorage(applicationContext)
@ -755,14 +599,19 @@ class MainActivity: FlutterActivity() {
private fun sendVoiceInteractionEvent(event: Map<String, Any>) {
if (voiceInteractionEventSink == null) {
Log.e(TAG, "无法发送语音交互事件:事件通道未准备好")
FileLogger.e(TAG, "无法发送语音交互事件:事件通道未准备好")
return
}
FileLogger.d(TAG, "准备发送事件到Flutter: ${event["type"]}")
runOnUiThread {
try {
voiceInteractionEventSink?.success(event)
FileLogger.d(TAG, "成功发送事件到Flutter: ${event["type"]}")
} catch (e: Exception) {
Log.e(TAG, "发送语音交互事件到Flutter失败: ${e.message}", e)
Log.e(TAG, "发送语音交互事件到Flutter失败: ${e.message}")
FileLogger.e(TAG, "发送语音交互事件到Flutter失败: ${e.message}")
}
}
}
@ -799,11 +648,58 @@ class MainActivity: FlutterActivity() {
}
// 释放资源
azureAsrHelper.dispose()
azureTtsHelper.dispose()
classicBluetoothHelper.dispose()
super.onDestroy()
}
/**
* 请求必要权限
*/
private fun requestRequiredPermissions() {
val permissionsToRequest = ArrayList<String>()
for (permission in REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(permission)
}
}
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(
this,
permissionsToRequest.toTypedArray(),
PERMISSION_REQUEST_CODE
)
}
}
/**
* 处理权限请求结果
*/
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE) {
val deniedPermissions = ArrayList<String>()
for (i in permissions.indices) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
deniedPermissions.add(permissions[i])
}
}
if (deniedPermissions.isNotEmpty()) {
// 记录未授权的权限
FileLogger.w(TAG, "未授权的权限: ${deniedPermissions.joinToString()}")
} else {
FileLogger.d(TAG, "所有必要权限已授权")
}
}
}
}

415
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionHandler.kt

@ -0,0 +1,415 @@
package com.yunqiinnovation.deepsound
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import org.json.JSONArray
import org.json.JSONObject
import android.util.Log
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.open_ai_service.SystemFunctionHandler
/**
* 语音交互处理器 - 处理语音识别、TTS和AI对话相关逻辑
*/
class VoiceInteractionHandler(
private val context: Context,
private val azureSpeechKey: String,
private val azureSpeechRegion: String,
private val openaiApiKey: String,
private val openaiBaseUrl: String = "",
private val openaiModel: String = "",
private val volcanoSpeechAppId: String,
private val volcanoSpeechAppToken: String,
private val mcpServer: String
) {
private val TAG = "VoiceInteractionHandler"
// Azure服务
private var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// OpenAI服务
private val openAIService = OpenAIService(context.applicationContext)
// 当前用户输入
private var currentUserInput = ""
// 状态
private var isInitialized = false
var isRecognitionActive = false
private set
var isTtsSpeaking = false
private set
var hasSpeechDetected = false
private set
// 回调
private var callback: InteractionCallback? = null
// 广播接收器
private val exitInteractionReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == SystemFunctionHandler.ACTION_EXIT_INTERACTION) {
Log.d(TAG, "收到退出交互广播")
stopRecognition()
}
}
}
/**
* 初始化
*/
fun initialize(): Boolean {
if (isInitialized) return true
try {
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply {
initialize(azureSpeechKey, azureSpeechRegion)
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper(context).apply {
initialize(azureSpeechKey, azureSpeechRegion)
}
// 初始化OpenAI服务
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer)
// 注册广播接收器
try {
Log.d(TAG, "注册退出交互广播接收器,包名=${context.packageName}, action=${SystemFunctionHandler.ACTION_EXIT_INTERACTION}")
context.registerReceiver(
exitInteractionReceiver,
IntentFilter(SystemFunctionHandler.ACTION_EXIT_INTERACTION),
Context.RECEIVER_NOT_EXPORTED
)
Log.d(TAG, "退出交互广播接收器注册成功")
} catch (e: Exception) {
// 广播注册失败不应该影响整个应用初始化
Log.e(TAG, "注册退出交互广播接收器失败: ${e.message}", e)
}
isInitialized = true
return true
} catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}", e)
return false
}
}
/**
* 设置回调
*/
fun setCallback(callback: InteractionCallback) {
this.callback = callback
}
/**
* 开始语音识别
*/
fun startRecognition() {
if (isRecognitionActive) return
// 检查录音权限
if (!checkRecordAudioPermission()) {
callback?.onError("需要录音权限,请在设置中授予权限")
return
}
isRecognitionActive = true
hasSpeechDetected = false
notifyStateChanged()
try {
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
hasSpeechDetected = true
stopTts()
notifyStateChanged()
}
}
override fun onResult(result: String, detectedLanguage: String) {
if (result.isNotEmpty()) {
notifyStateChanged()
processWithOpenAI(result)
}
// 重置状态,继续识别
hasSpeechDetected = false
}
override fun onSessionStarted() {
notifyStateChanged()
}
override fun onSessionStopped() {
isRecognitionActive = false
notifyStateChanged()
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
notifyStateChanged()
}
override fun onError(error: String) {
isRecognitionActive = false
callback?.onError("语音识别出错")
notifyStateChanged()
}
override fun onSuccess(message: String) {
// 处理成功事件
}
})
} catch (e: Exception) {
isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}", e)
callback?.onError("启动语音识别失败")
notifyStateChanged()
}
}
/**
* 停止语音识别
*/
fun stopRecognition() {
if (!isRecognitionActive) return
FileLogger.d(TAG, "停止语音识别")
try {
azureAsrHelper?.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(result: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别会话已停止")
notifyStateChanged()
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别已取消: $reason")
notifyStateChanged()
}
override fun onError(error: String) {
isRecognitionActive = false
FileLogger.e(TAG, "停止语音识别时出错: $error")
notifyStateChanged()
}
override fun onSuccess(message: String) {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别已停止: $message")
notifyStateChanged()
}
})
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}", e)
// 确保状态一致性
isRecognitionActive = false
notifyStateChanged()
}
}
/**
* 使用OpenAI处理语音识别结果
*/
private fun processWithOpenAI(text: String) {
// 保存当前用户输入,用于后续同步聊天记录
currentUserInput = text
Log.d(TAG, "用户 问题: $text")
Thread {
try {
val messages = JSONArray().apply {
put(openAIService.createUserMessage(text))
}
// 创建响应构建器
val responseBuilder = StringBuilder()
openAIService.sendMessageStream(
messages = messages,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
// 累加响应内容
responseBuilder.append(token)
}
override fun onComplete() {
// 处理完整响应
val response = responseBuilder.toString()
if (response.isNotEmpty()) {
// 播放AI回复
Log.d(TAG, "AI 回复: $response")
speakAIResponse(response)
// 同步聊天记录到Flutter端
sendChatHistoryUpdate("personal_assistant", text, response)
}
}
override fun onError(e: Exception) {
FileLogger.e(TAG, "AI流式处理出错: ${e.message}", e)
callback?.onError("AI处理出错")
}
override fun onFunctionCall(call: JSONObject) {
FileLogger.d(TAG, "processWithOpenAI 收到函数调用请求: ${call.getString("name")}")
}
}
)
} catch (e: Exception) {
FileLogger.e(TAG, "AI处理出错: ${e.message}", e)
callback?.onError("AI处理出错")
}
}.start()
}
/**
* 播放TTS
*/
fun playTts(text: String, callback: TtsCallback? = null) {
isTtsSpeaking = true
notifyStateChanged()
azureTtsHelper?.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
isTtsSpeaking = false
notifyStateChanged()
callback?.onComplete()
}
override fun onError(error: String) {
isTtsSpeaking = false
notifyStateChanged()
callback?.onError(error)
}
})
}
/**
* 停止TTS播放
*/
fun stopTts() {
if (isTtsSpeaking) {
azureTtsHelper?.stopSpeaking()
isTtsSpeaking = false
notifyStateChanged()
}
}
/**
* 播放AI回复
*/
private fun speakAIResponse(text: String) {
isTtsSpeaking = true
notifyStateChanged()
azureTtsHelper?.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
isTtsSpeaking = false
notifyStateChanged()
}
override fun onError(error: String) {
isTtsSpeaking = false
notifyStateChanged()
}
})
}
/**
* 释放资源
*/
fun dispose() {
// 停止语音识别
stopRecognition()
// 停止TTS播放
stopTts()
// 注销广播接收器
try {
context.unregisterReceiver(exitInteractionReceiver)
} catch (e: Exception) {
FileLogger.e(TAG, "注销广播接收器失败: ${e.message}", e)
}
// 释放Azure资源
azureAsrHelper?.let {
FileLogger.d(TAG, "关闭Azure ASR服务")
it.dispose()
}
azureTtsHelper?.let {
FileLogger.d(TAG, "关闭Azure TTS服务")
it.dispose()
}
FileLogger.d(TAG, "语音交互处理器资源已释放")
}
/**
* 检查录音权限
*/
private fun checkRecordAudioPermission(): Boolean {
val permission = android.Manifest.permission.RECORD_AUDIO
val result = context.checkCallingOrSelfPermission(permission)
return result == android.content.pm.PackageManager.PERMISSION_GRANTED
}
/**
* 通知状态变化
*/
private fun notifyStateChanged() {
callback?.onStateChanged(isRecognitionActive, isTtsSpeaking, hasSpeechDetected)
}
/**
* 发送聊天历史更新
*/
private fun sendChatHistoryUpdate(agentId: String, userMessage: String, assistantMessage: String) {
val intent = android.content.Intent(VoiceInteractionService.ACTION_CHAT_HISTORY_UPDATED).apply {
putExtra("agentId", agentId)
putExtra("userMessage", userMessage)
putExtra("assistantMessage", assistantMessage)
putExtra("timestamp", System.currentTimeMillis())
setPackage(context.packageName)
}
// 发送广播
context.sendBroadcast(intent)
}
/**
* 交互回调接口
*/
interface InteractionCallback {
fun onStateChanged(isRecognitionActive: Boolean, isTtsSpeaking: Boolean, hasSpeechDetected: Boolean)
fun onError(message: String)
fun onPromptRequest(message: String)
}
/**
* TTS回调接口
*/
interface TtsCallback {
fun onComplete()
fun onError(error: String)
}
}

383
android/app/src/main/kotlin/com/example/deep_voice/VoiceInteractionService.kt → android/app/src/main/kotlin/com/yunqiinnovation/deepsound/VoiceInteractionService.kt

@ -22,16 +22,21 @@ import android.os.Handler
import android.os.Looper
import java.util.concurrent.atomic.AtomicBoolean
import org.json.JSONArray
import org.json.JSONObject
import android.media.MediaPlayer
import android.media.AudioAttributes
import android.net.Uri
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.volcano_speech.VolcanoTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService
/**
* 后台语音交互 Service:
* 1) 前台服务,确保不会被系统轻易杀死
* 2) MediaSession 捕获蓝牙耳机按键
* 3) 处理录音/语音识别
* 3) 负责唤醒控制和服务生命周期管理
*/
class VoiceInteractionService : Service() {
@ -41,7 +46,7 @@ class VoiceInteractionService : Service() {
private const val CHANNEL_ID = "voice_interaction_channel"
// 语音识别超时时间(毫秒)
private const val RECOGNITION_TIMEOUT = 8000L
private const val RECOGNITION_TIMEOUT = 10000L
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
@ -53,14 +58,12 @@ class VoiceInteractionService : Service() {
const val ACTION_RECOGNITION_STARTED = "com.yunqiinnovation.deepsound.ACTION_RECOGNITION_STARTED"
const val ACTION_PAUSE_VOICE_INTERACTION = "com.yunqiinnovation.deepsound.ACTION_PAUSE_VOICE_INTERACTION"
const val ACTION_CHAT_HISTORY_UPDATED = "com.yunqiinnovation.deepsound.ACTION_CHAT_HISTORY_UPDATED"
const val ACTION_ENTER_TRANSLATION_MODE = "com.yunqiinnovation.deepsound.ACTION_ENTER_TRANSLATION_MODE"
}
// 服务状态
private var isActive = false // 服务是否活跃
private var isRecognitionActive = false // 语音识别是否活跃
private var isTimeoutPaused = false // 是否因超时暂停
private var hasSpeechDetected = false // 是否检测到语音
private var isTtsSpeaking = false // 是否正在播放TTS
// 按键处理
private var lastKeyEventTime = 0L
@ -69,16 +72,12 @@ class VoiceInteractionService : Service() {
// 活动时间
private var lastActivityTime = 0L
// 当前用户输入
private var currentUserInput = ""
// 服务组件
private lateinit var mediaSession: MediaSessionCompat
private lateinit var audioManager: AudioManager
private lateinit var azureAsrHelper: AzureAsrHelper
private lateinit var azureTtsHelper: AzureTtsHelper
private lateinit var volcanoAIService: VolcanoAIService
// 语音交互处理器
private lateinit var voiceInteractionHandler: VoiceInteractionHandler
// 定时器
private val handler = Handler(Looper.getMainLooper())
@ -88,15 +87,6 @@ class VoiceInteractionService : Service() {
handler.postDelayed(this, 1000) // 每秒执行一次
}
}
// 系统提示词
private val systemPrompt = """
你是一个智能语音助手,能够简洁明了地回答用户的问题。
请保持回答简短、准确,避免过长的解释。
如果用户的问题不清楚,请礼貌地请求澄清。
不要使用复杂的术语,除非用户明确要求。
用户用语音和你交互.
""".trimIndent()
// 添加媒体播放器
private var audioPlayer: MiniMediaPlayer? = null
@ -115,7 +105,9 @@ class VoiceInteractionService : Service() {
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
FileLogger.d(TAG, "AudioManager初始化完成")
initServices()
// 初始化语音交互处理器
initVoiceInteractionHandler()
initMediaSession()
registerMediaButtonReceiver()
@ -125,7 +117,6 @@ class VoiceInteractionService : Service() {
// 设置为媒体播放状态
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
// FileLogger.d(TAG, "设置播放状态为STATE_PAUSED")
// 启动监控和前台服务
startMonitoring()
@ -139,28 +130,27 @@ class VoiceInteractionService : Service() {
*/
private fun resetState() {
isActive = false
isRecognitionActive = false
isTimeoutPaused = false
hasSpeechDetected = false
isTtsSpeaking = false
}
/**
* 初始化所有服务
* 初始化语音交互处理器
*/
private fun initServices() {
// 创建新的Azure服务实例
FileLogger.d(TAG, "创建新的Azure服务实例")
azureAsrHelper = AzureAsrHelper(this)
azureTtsHelper = AzureTtsHelper(this)
private fun initVoiceInteractionHandler() {
FileLogger.d(TAG, "初始化语音交互处理器")
// 尝试从静态变量获取配置
var subscriptionKey = MainActivity.azureSpeechKey
var serviceRegion = MainActivity.azureSpeechRegion
var volcanoKey = MainActivity.volcanoAiApiKey
var openaiKey = MainActivity.openaiApiKey
var openaiBaseUrl = MainActivity.openaiBaseUrl ?: "" // OpenAI API基本URL
var openaiModel = MainActivity.openaiModel ?: "" // OpenAI模型
var volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: ""
var volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: ""
var mcpServer = MainActivity.mcpServer ?: ""
// 如果静态变量中没有配置,尝试从加密存储中加载
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || volcanoKey.isEmpty()) {
if (subscriptionKey.isEmpty() || serviceRegion.isEmpty() || openaiKey.isEmpty()) {
FileLogger.d(TAG, "静态变量中的配置信息不完整,尝试从加密存储加载")
// 从加密存储加载密钥
@ -170,36 +160,47 @@ class VoiceInteractionService : Service() {
// 更新本地变量
subscriptionKey = MainActivity.azureSpeechKey
serviceRegion = MainActivity.azureSpeechRegion
volcanoKey = MainActivity.volcanoAiApiKey
openaiKey = MainActivity.openaiApiKey
openaiBaseUrl = MainActivity.openaiBaseUrl ?: ""
openaiModel = MainActivity.openaiModel ?: ""
volcanoSpeechAppId = MainActivity.volcanoSpeechAppId ?: ""
volcanoSpeechAppToken = MainActivity.volcanoSpeechAppToken ?: ""
mcpServer = MainActivity.mcpServer ?: ""
FileLogger.d(TAG, "已从加密存储加载配置信息")
} else {
FileLogger.e(TAG, "无法从加密存储加载配置信息")
}
}
// 初始化语音服务
if (subscriptionKey.isNotEmpty() && serviceRegion.isNotEmpty()) {
// 初始化ASR
azureAsrHelper.initialize(subscriptionKey, serviceRegion, arrayOf("zh-CN"))
// 初始化语音交互处理器
voiceInteractionHandler = VoiceInteractionHandler(applicationContext,
subscriptionKey, serviceRegion,
openaiKey, openaiBaseUrl, openaiModel,
volcanoSpeechAppId, volcanoSpeechAppToken,
mcpServer)
// 初始化回调
voiceInteractionHandler.setCallback(object : VoiceInteractionHandler.InteractionCallback {
override fun onStateChanged(isRecognitionActive: Boolean, isTtsSpeaking: Boolean, hasSpeechDetected: Boolean) {
// 更新活动时间
updateLastActivityTime()
}
// 初始化TTS
azureTtsHelper.initialize(subscriptionKey, serviceRegion, "zh-CN")
override fun onError(message: String) {
playNotification(message)
}
FileLogger.d(TAG, "Azure语音服务已初始化")
} else {
FileLogger.e(TAG, "Azure配置信息不完整,无法初始化Azure服务")
}
// 初始化火山AI服务
volcanoAIService = VolcanoAIService()
override fun onPromptRequest(message: String) {
playPrompt(message)
}
})
// 初始化火山AI服务
if (volcanoKey.isNotEmpty()) {
volcanoAIService.initialize(volcanoKey)
FileLogger.d(TAG, "火山AI服务已初始化")
// 初始化处理器
val initialized = voiceInteractionHandler.initialize()
if (initialized) {
FileLogger.d(TAG, "语音交互处理器初始化成功")
} else {
FileLogger.e(TAG, "火山AI配置信息不完整,无法初始化火山AI服务")
FileLogger.e(TAG, "语音交互处理器初始化失败")
}
}
@ -302,24 +303,23 @@ class VoiceInteractionService : Service() {
if (!isActive) {
isActive = true
}
// FileLogger.d(TAG, "服务状态: isActive=${isActive}, isRecognitionActive=${voiceInteractionHandler.isRecognitionActive}, isTimeoutPaused=${isTimeoutPaused}")
// 检查语音识别状态
if (isRecognitionActive) {
if (voiceInteractionHandler.isRecognitionActive) {
val currentTime = System.currentTimeMillis()
val elapsedTime = currentTime - lastActivityTime
// 如果超过5秒没有检测到语音,且不在TTS播放中,暂停语音识别
if (!hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT) {
// FileLogger.d(TAG, "hasSpeechDetected=${voiceInteractionHandler.hasSpeechDetected}, isTtsSpeaking=${voiceInteractionHandler.isTtsSpeaking}, elapsedTime=${elapsedTime}")
// 如果超过指定时间没有检测到语音,且不在TTS播放中,暂停语音识别
if (!voiceInteractionHandler.hasSpeechDetected &&
!voiceInteractionHandler.isTtsSpeaking &&
elapsedTime >= RECOGNITION_TIMEOUT) {
FileLogger.d(TAG, "超过${RECOGNITION_TIMEOUT/1000}秒未检测到语音,停止识别")
isTimeoutPaused = true
playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。")
// 停止语音识别并确保资源完全释放
stopVoiceRecognition()
// 清理识别状态
isRecognitionActive = false
hasSpeechDetected = false
// 停止语音识别
voiceInteractionHandler.stopRecognition()
}
}
}
@ -340,14 +340,12 @@ class VoiceInteractionService : Service() {
lastKeyEventTime = currentTime
// 停止当前TTS播放
stopCurrentTTS()
voiceInteractionHandler.stopTts()
setPlaybackState(PlaybackStateCompat.STATE_PLAYING)
// 播放提示音
playPrompt("我在!")
// audioPlayer?.play(R.raw.listening)
// 设置为播放状态
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
@ -355,221 +353,48 @@ class VoiceInteractionService : Service() {
// 重置超时暂停标志
isTimeoutPaused = false
// 启动或重置语音识别
if (!isRecognitionActive) {
// 启动语音识别
if (!voiceInteractionHandler.isRecognitionActive) {
FileLogger.d(TAG, "语音识别未激活,开始启动")
// 如果之前是因为超时暂停,重新初始化语音识别组件
// if (isTimeoutPaused) {
// Log.d(TAG, "之前因超时暂停,重新初始化Azure服务")
// restartAsr()
// }
// 通知 Flutter 语音识别已启动
notifyRecognitionStarted()
startVoiceRecognition()
// 启动语音识别
voiceInteractionHandler.startRecognition()
} else {
FileLogger.d(TAG, "语音识别已激活,更新活动时间")
updateLastActivityTime()
hasSpeechDetected = false
}
}
/**
* 开始语音识别
*/
private fun startVoiceRecognition() {
if (isRecognitionActive) return
// 检查录音权限
if (!checkRecordAudioPermission()) {
playNotification("需要录音权限,请在设置中授予权限")
return
}
// 通知 Flutter 语音识别已启动
notifyRecognitionStarted()
isActive = true
isRecognitionActive = true
hasSpeechDetected = false
updateLastActivityTime()
try {
azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
}
}
override fun onResult(result: String, detectedLanguage: String) {
if (result.isNotEmpty()) {
processWithVolcanoAI(result)
}
// 重置状态,继续识别
hasSpeechDetected = false
updateLastActivityTime()
}
override fun onSessionStarted() {
updateLastActivityTime()
}
override fun onSessionStopped() {
isRecognitionActive = false
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
}
override fun onError(error: String) {
isRecognitionActive = false
playNotification("语音识别出错")
}
})
} catch (e: Exception) {
isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}", e)
playNotification("启动语音识别失败")
}
}
/**
* 停止语音识别
*/
private fun stopVoiceRecognition() {
if (!isRecognitionActive) return
FileLogger.d(TAG, "停止语音识别")
try {
azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(result: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别会话已停止")
}
override fun onCanceled(reason: String, errorDetails: String) {
isRecognitionActive = false
FileLogger.d(TAG, "语音识别已取消: $reason")
}
override fun onError(error: String) {
isRecognitionActive = false
FileLogger.e(TAG, "停止语音识别时出错: $error")
}
})
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}", e)
// 确保状态一致性
isRecognitionActive = false
}
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
isRecognitionActive = false
hasSpeechDetected = false
// 不重置 isTimeoutPaused,保留暂停原因
}
/**
* 使用VolcanoAI处理语音识别结果
*/
private fun processWithVolcanoAI(text: String) {
// 保存当前用户输入,用于后续同步聊天记录
currentUserInput = text
Thread {
try {
val messages = JSONArray().apply {
put(volcanoAIService.createUserMessage(text))
}
val response = volcanoAIService.sendMessage(messages, systemPrompt)
// 播放AI回复
speakAIResponse(response)
// 同步聊天记录到Flutter端
notifyChatHistoryUpdated("personal_assistant", text, response)
} catch (e: Exception) {
FileLogger.e(TAG, "AI处理出错: ${e.message}")
playNotification("AI处理出错")
}
}.start()
}
/**
* 播放AI回复
*/
private fun speakAIResponse(text: String) {
isTtsSpeaking = true
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
isTtsSpeaking = false
updateLastActivityTime()
}
override fun onError(error: String) {
isTtsSpeaking = false
}
})
}
/**
* 播放提示音
*/
private fun playPrompt(message: String) {
isTtsSpeaking = true
azureTtsHelper.speakText(message, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) { isTtsSpeaking = false }
override fun onError(error: String) { isTtsSpeaking = false }
})
voiceInteractionHandler.playTts(message)
}
/**
* 播放通知提示音
*/
private fun playNotification(message: String) {
isTtsSpeaking = true
// 更新播放状态为播放中,增加接收蓝牙按键事件的几率
setPlaybackState(PlaybackStateCompat.STATE_PLAYING)
// 确保媒体会话处于活跃状态
mediaSession.isActive = true
azureTtsHelper.speakText(message, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
isTtsSpeaking = false
voiceInteractionHandler.playTts(message, object : VoiceInteractionHandler.TtsCallback {
override fun onComplete() {
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
}
override fun onError(error: String) {
isTtsSpeaking = false
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
}
})
}
/**
* 停止当前TTS播放
*/
private fun stopCurrentTTS() {
if (isTtsSpeaking) {
azureTtsHelper.stopSpeaking()
isTtsSpeaking = false
}
}
/**
* 更新最后活动时间
*/
@ -603,15 +428,6 @@ class VoiceInteractionService : Service() {
}
}
/**
* 检查录音权限
*/
private fun checkRecordAudioPermission(): Boolean {
val permission = android.Manifest.permission.RECORD_AUDIO
val result = applicationContext.checkCallingOrSelfPermission(permission)
return result == android.content.pm.PackageManager.PERMISSION_GRANTED
}
/**
* 启动前台服务
*/
@ -746,38 +562,32 @@ class VoiceInteractionService : Service() {
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
FileLogger.d(TAG, "onDestroy - 语音交互服务正在销毁")
// 释放音频播放器
audioPlayer?.release()
audioPlayer = null
super.onDestroy()
FileLogger.d(TAG, "onDestroy - 语音交互服务即将销毁")
// 停止监控
// 停止服务监控
stopMonitoring()
// 停止语音识别
if (isRecognitionActive) {
stopVoiceRecognition()
}
voiceInteractionHandler.stopRecognition()
// 停止媒体会话
mediaSession.release()
FileLogger.d(TAG, "媒体会话已释放")
// 停止TTS
stopCurrentTTS()
voiceInteractionHandler.stopTts()
// 释放 MediaSession
mediaSession.release()
// 释放语音交互处理器资源
voiceInteractionHandler.dispose()
// 释放Azure服务实例
FileLogger.d(TAG, "释放Azure服务实例")
azureAsrHelper.dispose()
azureTtsHelper.dispose()
// 关闭音频播放器
audioPlayer?.release()
// 更新服务状态
// 设置服务状态
isRunning.set(false)
// 关闭日志系统
FileLogger.shutdown()
super.onDestroy()
FileLogger.d(TAG, "语音交互服务已销毁")
}
/**
@ -788,11 +598,11 @@ class VoiceInteractionService : Service() {
FileLogger.d(TAG, "暂停后台语音交互(来自Flutter的请求)")
// 停止当前TTS播放
stopCurrentTTS()
voiceInteractionHandler.stopTts()
// 停止语音识别
if (isRecognitionActive) {
stopVoiceRecognition()
if (voiceInteractionHandler.isRecognitionActive) {
voiceInteractionHandler.stopRecognition()
}
// 设置为暂停状态,但保持服务活跃
@ -804,10 +614,6 @@ class VoiceInteractionService : Service() {
* 通知 Flutter 端聊天记录已更新
*/
private fun notifyChatHistoryUpdated(agentId: String, userMessage: String, assistantMessage: String) {
FileLogger.d(TAG, "通知Flutter聊天记录已更新: agentId=$agentId")
FileLogger.d(TAG, "用户消息: ${userMessage.take(50)}...")
FileLogger.d(TAG, "助手回复: ${assistantMessage.take(50)}...")
// 创建广播 Intent
val intent = Intent(ACTION_CHAT_HISTORY_UPDATED).apply {
putExtra("agentId", agentId)
@ -818,7 +624,6 @@ class VoiceInteractionService : Service() {
// 发送广播
sendBroadcast(intent)
FileLogger.d(TAG, "已发送聊天记录广播")
}
/**
@ -831,6 +636,7 @@ class VoiceInteractionService : Service() {
val intent = Intent(ACTION_RECOGNITION_STARTED).apply {
// 可以添加额外数据
putExtra("timestamp", System.currentTimeMillis())
putExtra("packageName", applicationContext.packageName)
}
// 发送广播
@ -909,5 +715,4 @@ class VoiceInteractionService : Service() {
*/
fun isPlaying() = mediaPlayer?.isPlaying == true
}
}

0
android/app/src/main/kotlin/com/example/deep_voice/core/utils/FileLogger.kt → android/app/src/main/kotlin/com/yunqiinnovation/deepsound/core/utils/FileLogger.kt

19
android/build.gradle.kts

@ -1,7 +1,15 @@
allprojects {
repositories {
mavenLocal()
google()
mavenCentral()
maven { url = uri("https://storage.googleapis.com/download.flutter.io") }
// 添加火山引擎Maven仓库
maven {
url = uri("https://artifact.bytedance.com/repository/Volcengine/")
}
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
}
}
@ -20,3 +28,14 @@ subprojects {
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10")
// 其他依赖...
}
}

20
android/settings.gradle.kts

@ -16,6 +16,7 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
mavenLocal()
google()
mavenCentral()
gradlePluginPortal()
@ -24,9 +25,22 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader")
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.android.application") version "8.8.0" apply false
id("com.android.library") version "8.8.0" apply false
id("org.jetbrains.kotlin.android") version "2.1.10" apply false
}
include(":app")
include(":azure_speech")
include(":open_ai_service")
include(":volcano_speech")
include(":chat_storage")
include(":jieli")
include(":spotify_sdk")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android")
project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android")
project(":jieli").projectDir = file("../local_plugins/jieli/android")
project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android")

21
azure/LICENSE

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Your Company
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

66
azure/README.md

@ -1,66 +0,0 @@
# Azure Speech Recognition
A Flutter plugin for Microsoft Azure Speech services, providing both speech recognition (ASR) and text-to-speech (TTS) capabilities.
## Features
- Speech-to-text (Azure Speech Recognition)
- Text-to-speech (Azure Speech Synthesis)
- Support for multiple languages
- Language detection
- Continuous recognition
- Streaming synthesis
## Getting Started
### Prerequisites
- Azure Speech service subscription key
- Azure Speech service region
### Installation
Add this to your package's `pubspec.yaml` file:
```yaml
dependencies:
azure_speech_recognition:
path: ./azure
```
### Usage
```dart
import 'package:azure_speech_recognition/azure_speech_recognition.dart';
// Initialize the service
await AzureSpeechRecognition.initialize(
subscriptionKey: 'your_subscription_key',
region: 'your_region',
supportedLanguages: ['zh-CN', 'en-US'],
);
// Start continuous recognition
await AzureSpeechRecognition.startContinuousRecognition();
// Listen for recognition events
AzureSpeechRecognition.onRecognitionEvent.listen((event) {
if (event['type'] == 'result') {
print('Recognized: ${event['text']}');
print('Detected language: ${event['detectedLanguage']}');
}
});
// Stop recognition when done
await AzureSpeechRecognition.stopContinuousRecognition();
// Speak text
await AzureSpeechRecognition.speakText('Hello, world!');
// Clean up
await AzureSpeechRecognition.dispose();
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.

757
azure/ios/Classes/AzureAsrHelper.swift

@ -1,757 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
import AudioToolbox
/// Azure ASR工具类,负责实现语音识别服务接口
@available(iOS 13.0, *)
class AzureAsrHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音识别相关
private var speechConfig: SPXSpeechConfiguration?
private var recognizer: SPXSpeechRecognizer?
private var audioConfig: SPXAudioConfiguration?
private var pushStream: SPXPushAudioInputStream?
/// 音频处理相关
private var audioProcessor: CustomAudioProcessor?
private var isProcessingAudio = false
private var audioProcessingTimer: Timer?
/// 状态标志
private var isInitialized = false
private var _isContinuousRecognitionActive = false
/// 当前语言和支持的语言
private var currentLanguage = "zh-CN"
private var supportedLanguages: [String] = ["zh-CN", "en-US"]
private var isAutoDetectLanguage = false
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
super.init()
}
deinit {
dispose()
}
// MARK: - ASR Service 接口实现
/// 初始化语音识别服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - supportedLanguages: 支持的语言代码数组 (可选)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, supportedLanguages: [String]? = nil) -> Bool {
print("[AzureAsrHelper] 初始化 Azure 语音服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["message": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 设置语言
if let languages = supportedLanguages, !languages.isEmpty {
self.supportedLanguages = languages
}
// 根据支持的语言数量决定是否启用自动语言检测
isAutoDetectLanguage = self.supportedLanguages.count >= 2
// 如果只有一种语言,设置为当前语言
if !isAutoDetectLanguage && !self.supportedLanguages.isEmpty {
currentLanguage = self.supportedLanguages[0]
}
// 创建识别器和设置回调
if !createRecognizerAndSetupCallbacks() {
return false
}
print("[AzureAsrHelper] Azure 语音服务初始化成功")
isInitialized = true
return true
}
/// 创建识别器并设置回调
private func createRecognizerAndSetupCallbacks() -> Bool {
// 释放之前的 recognizer
recognizer = nil
audioConfig = nil
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置音频输入参数
try setupAudioSession()
// 创建自定义推送流,替代默认的麦克风输入
pushStream = try SPXPushAudioInputStream()
audioConfig = try SPXAudioConfiguration(streamInput: pushStream!)
// 初始化自定义音频处理器
audioProcessor = CustomAudioProcessor()
// 设置语言配置
if isAutoDetectLanguage {
// 设置自动语言检测
speechConfig?.setPropertyTo("Continuous", by: SPXPropertyId.speechServiceConnectionLanguageIdMode)
// 创建自动语言检测配置
let autoDetectSourceLanguageConfig = try SPXAutoDetectSourceLanguageConfiguration(supportedLanguages)
// 创建识别器
recognizer = try SPXSpeechRecognizer(
speechConfiguration: speechConfig!,
autoDetectSourceLanguageConfiguration: autoDetectSourceLanguageConfig,
audioConfiguration: audioConfig!
)
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
// 创建识别器
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
}
// 设置所有回调
setupAllCallbacks()
return true
} catch {
print("[AzureAsrHelper] 错误: 创建识别器失败: \(error.localizedDescription)")
eventHandler("error", ["message": "创建识别器失败: \(error.localizedDescription)"])
return false
}
}
/// 设置音频会话
private func setupAudioSession() throws {
let audioSession = AVAudioSession.sharedInstance()
// 使用playAndRecord类别允许同时录音和播放
try audioSession.setCategory(.playAndRecord,
mode: .voiceChat, // 使用voiceChat模式能够更好地支持回音消除
options: [.allowBluetooth, .defaultToSpeaker, .allowAirPlay, .mixWithOthers])
// 设置首选的输入和输出
let currentRoute = audioSession.currentRoute
// 获取当前是否连接了耳机或外部麦克风
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 如果没有耳机,明确启用内置麦克风和扬声器的回音消除
if !hasHeadphones {
try audioSession.setMode(.voiceChat) // 语音聊天模式有更强的回音消除
// 启用回音消除和噪声抑制
try audioSession.setInputGain(0.8) // 适当降低输入增益以减少扬声器音频被麦克风捕获的可能性
} else {
// 耳机模式,可以使用不同的设置
try audioSession.setMode(.voiceChat)
try audioSession.setInputGain(1.0)
}
// 设置合适的采样率
try audioSession.setPreferredSampleRate(16000.0) // Azure语音识别推荐的采样率
try audioSession.setPreferredIOBufferDuration(0.01) // 较小的缓冲区大小以减少延迟
// 激活音频会话
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
print("[AzureAsrHelper] 音频会话配置成功,已启用回音消除")
}
/// 设置所有回调
private func setupAllCallbacks() {
guard let recognizer = recognizer else { return }
// 最终识别结果
recognizer.addRecognizedEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("result", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 识别中事件
recognizer.addRecognizingEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == SPXResultReason.recognizingSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
// print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
self.eventHandler("recognizing", [
"text": event.result.text ?? "",
"detectedLanguage": detectedLanguage
])
}
}
// 会话事件
recognizer.addSessionStartedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已开始")
self._isContinuousRecognitionActive = true
self.eventHandler("sessionStarted", [:])
}
recognizer.addSessionStoppedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已结束")
self._isContinuousRecognitionActive = false
self.eventHandler("sessionStopped", [:])
}
// 取消事件
recognizer.addCanceledEventHandler { [weak self] _, event in
guard let self = self else { return }
let reason = event.reason.rawValue
let errorDetails = event.errorDetails ?? "未知错误"
print("[AzureAsrHelper] 识别取消: \(errorDetails)")
self.eventHandler("canceled", [
"reason": reason,
"errorDetails": errorDetails
])
self._isContinuousRecognitionActive = false
}
}
/// 执行一次性语音识别
/// - Returns: 是否成功启动识别
func recognizeOnce() -> Bool {
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
return false
}
// 如果正在连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
do {
// 启动音频处理
startAudioProcessing()
// 通知会话开始
eventHandler("sessionStarted", [:])
// 执行识别
try recognizer?.recognizeOnceAsync { [weak self] result in
guard let self = self else { return }
// 停止音频处理
self.stopAudioProcessing()
if result.reason == SPXResultReason.recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: result)
self.eventHandler("result", [
"text": result.text ?? "",
"detectedLanguage": detectedLanguage
])
} else if result.reason == SPXResultReason.noMatch {
print("[AzureAsrHelper] 无匹配结果")
self.eventHandler("noMatch", [:])
} else if result.reason == SPXResultReason.canceled {
do {
let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result)
let errorDetails = details.errorDetails ?? "未知错误"
self.eventHandler("error", ["message": "识别取消: \(errorDetails)"])
} catch {
print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)")
self.eventHandler("error", ["message": "识别取消,无法获取详细原因"])
}
}
}
return true
} catch {
print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)")
eventHandler("error", ["message": "识别异常: \(error.localizedDescription)"])
stopAudioProcessing()
return false
}
}
/// 开始连续语音识别
/// - Returns: 是否成功启动识别
func startContinuousRecognition() -> Bool {
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler("error", ["message": "语音服务未初始化"])
return false
}
// 如果已经在进行连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
// 重新确保音频设置正确
do {
try setupAudioSession()
} catch {
print("[AzureAsrHelper] 警告: 设置音频会话失败: \(error.localizedDescription)")
}
do {
// 启动音频处理
startAudioProcessing()
// 启动连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别开始")
return true
} catch {
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "开始连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
stopAudioProcessing()
return false
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
// 停止音频处理
stopAudioProcessing()
if !_isContinuousRecognitionActive || recognizer == nil {
return true
}
do {
try recognizer?.stopContinuousRecognition()
_isContinuousRecognitionActive = false
print("[AzureAsrHelper] 连续识别已停止")
return true
} catch {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
eventHandler("error", ["message": "停止连续识别失败: \(error.localizedDescription)"])
_isContinuousRecognitionActive = false
return false
}
}
/// 检查连续识别是否活跃
/// - Returns: 连续识别是否处于活跃状态
func isContinuousRecognitionActive() -> Bool {
return _isContinuousRecognitionActive
}
/// 释放资源
func dispose() {
print("[AzureAsrHelper] 释放资源")
// 停止音频处理
stopAudioProcessing()
// 停止连续识别
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 释放音频会话
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print("[AzureAsrHelper] 警告: 释放音频会话失败: \(error.localizedDescription)")
}
// 释放资源
recognizer = nil
speechConfig = nil
audioConfig = nil
pushStream = nil
audioProcessor = nil
// 重置状态
_isContinuousRecognitionActive = false
isInitialized = false
}
/// 从结果中获取检测到的语言
private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
if isAutoDetectLanguage {
do {
let langResult = try SPXAutoDetectSourceLanguageResult(result)
return langResult.language ?? currentLanguage
} catch {
print("[AzureAsrHelper] 错误: 获取检测到的语言失败: \(error.localizedDescription)")
return currentLanguage
}
} else {
return currentLanguage
}
}
// MARK: - 音频处理
/// 开始音频处理
private func startAudioProcessing() {
guard !isProcessingAudio, let audioProcessor = audioProcessor else { return }
isProcessingAudio = true
// 启动音频处理器
if !audioProcessor.startRecord() {
print("[AzureAsrHelper] 错误: 启动音频处理器失败")
eventHandler("error", ["message": "启动音频处理器失败"])
return
}
// 启动音频处理定时器
audioProcessingTimer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in
guard let self = self, self.isProcessingAudio, let processor = self.audioProcessor, let stream = self.pushStream else {
return
}
// 读取处理后的音频数据
var bytes = [UInt8](repeating: 0, count: 2560)
let bytesRead = processor.read(bytes: &bytes)
if bytesRead > 0 {
// 推送数据到Azure语音服务
let data = Data(bytes: bytes, count: bytesRead)
stream.write(data)
// 通知音频数据可用
self.eventHandler("audioData", ["data": bytes])
}
}
print("[AzureAsrHelper] 音频处理已启动")
}
/// 停止音频处理
private func stopAudioProcessing() {
// 停止定时器
audioProcessingTimer?.invalidate()
audioProcessingTimer = nil
// 停止音频处理器
audioProcessor?.stopRecord()
isProcessingAudio = false
print("[AzureAsrHelper] 音频处理已停止")
}
}
// MARK: - 自定义音频处理器
@available(iOS 13.0, *)
class CustomAudioProcessor: NSObject {
// 音频单元
private var ioUnit: AudioUnit?
// 音频格式
private var audioFormat: AudioStreamBasicDescription
// 音频缓冲
private var audioBufferList: AudioBufferList
private var audioList: [Float] = []
private let audioListQueue = DispatchQueue(label: "audioListQueue")
// 回音消除状态
private var isEchoCancellationEnabled = true
override init() {
// 设置音频格式 - 16kHz, 16位, 单声道
audioFormat = AudioStreamBasicDescription(
mSampleRate: 16000.0,
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked,
mBytesPerPacket: 2,
mFramesPerPacket: 1,
mBytesPerFrame: 2,
mChannelsPerFrame: 1,
mBitsPerChannel: 16,
mReserved: 0
)
// 初始化音频缓冲
audioBufferList = AudioBufferList(
mNumberBuffers: 1,
mBuffers: AudioBuffer(
mNumberChannels: 1,
mDataByteSize: 4096,
mData: malloc(4096)
)
)
super.init()
}
deinit {
stopRecord()
free(audioBufferList.mBuffers.mData)
}
/// 启动音频处理
/// - Returns: 是否成功启动
func startRecord() -> Bool {
print("[CustomAudioProcessor] 配置音频单元")
// 创建音频组件描述 - 使用VoiceProcessingIO类型获取回音消除
var ioUnitDescription = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_VoiceProcessingIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
// 查找音频组件
guard let ioUnitRef = AudioComponentFindNext(nil, &ioUnitDescription) else {
print("[CustomAudioProcessor] 错误: 未找到音频组件")
return false
}
// 创建音频单元实例
if checkError(AudioComponentInstanceNew(ioUnitRef, &ioUnit), "创建音频单元") {
ioUnit = nil
return false
}
// 启用输入端口
var enableInput: UInt32 = 1
let kInputBus: AudioUnitElement = 1
let kOutputBus: AudioUnitElement = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, kInputBus, &enableInput,
UInt32(MemoryLayout<UInt32>.size)), "启用输入端口") {
return false
}
// 禁用输出端口 (我们只需要输入)
var enableOutput: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, kOutputBus,
&enableOutput, UInt32(MemoryLayout<UInt32>.size)), "禁用输出端口") {
return false
}
// 设置缓冲区分配标志
var flag: UInt32 = 0
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout<UInt32>.size)), "设置缓冲区分配标志") {
return false
}
// 设置音频格式
let size = UInt32(MemoryLayout<AudioStreamBasicDescription>.size)
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output, kInputBus, &audioFormat, size), "设置输入总线输出范围的流格式") {
return false
}
if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, kOutputBus, &audioFormat, size), "设置输出总线输入范围的流格式") {
return false
}
// 启用回音消除
if isEchoCancellationEnabled {
var echoCancellation: UInt32 = 1
AudioUnitSetProperty(ioUnit!, kAUVoiceIOProperty_BypassVoiceProcessing,
kAudioUnitScope_Global, 0, &echoCancellation, UInt32(MemoryLayout<UInt32>.size))
}
// 设置输入回调 - 当有新音频数据时调用
var inputCallback = AURenderCallbackStruct(
inputProc: CustomAudioProcessor.onAudioDataAvailable,
inputProcRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
)
if checkError(AudioUnitSetProperty(ioUnit!,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global, kInputBus,
&inputCallback, UInt32(MemoryLayout<AURenderCallbackStruct>.size)), "设置输入回调") {
return false
}
// 初始化音频单元
var hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
while hasError {
Thread.sleep(forTimeInterval: 0.1)
hasError = checkError(AudioUnitInitialize(ioUnit!), "初始化音频单元")
}
// 启动音频单元
hasError = checkError(AudioOutputUnitStart(ioUnit!), "启动音频单元")
print("[CustomAudioProcessor] 音频处理器已启动,回音消除\(isEchoCancellationEnabled ? "已启用" : "已禁用")")
return !hasError
}
/// 停止音频处理
func stopRecord() {
print("[CustomAudioProcessor] 停止音频处理器")
if let ioUnit = ioUnit {
// 停止音频单元
_ = checkError(AudioOutputUnitStop(ioUnit), "停止音频单元")
// 关闭音频单元
_ = checkError(AudioUnitUninitialize(ioUnit), "反初始化音频单元")
_ = checkError(AudioComponentInstanceDispose(ioUnit), "释放音频单元")
self.ioUnit = nil
}
// 清空音频数据缓冲
audioListQueue.sync {
audioList.removeAll()
}
}
/// 音频数据回调 - 当有新的音频数据可用时调用
private static let onAudioDataAvailable: AURenderCallback = { inRefCon, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, ioData in
// 获取实例
let processor = Unmanaged<CustomAudioProcessor>.fromOpaque(inRefCon).takeUnretainedValue()
// 计算预期数据大小
let expectedDataByteSize = inNumberFrames * processor.audioFormat.mBytesPerFrame
// 确保缓冲区足够大
if processor.audioBufferList.mBuffers.mDataByteSize < expectedDataByteSize {
processor.audioBufferList.mBuffers.mData = realloc(processor.audioBufferList.mBuffers.mData, Int(expectedDataByteSize))
processor.audioBufferList.mBuffers.mDataByteSize = expectedDataByteSize
}
// 渲染音频数据
let status = processor.checkOSStatus(AudioUnitRender(processor.ioUnit!, ioActionFlags, inTimeStamp,
inBusNumber, inNumberFrames, &processor.audioBufferList),
"渲染音频数据")
// 将Int16数据转换为浮点数据进行处理
var audioDataFloat = [Float](repeating: 0.0, count: Int(inNumberFrames))
let buffer = processor.audioBufferList.mBuffers
let bufferData = buffer.mData!.assumingMemoryBound(to: Int16.self)
for j in 0..<Int(buffer.mDataByteSize / UInt32(MemoryLayout<Int16>.size)) {
// 归一化到[-1.0, 1.0]范围
audioDataFloat[j] = Float(bufferData[j]) / 32768.0
}
// 应用附加处理 (如有需要)
// processor.applyAdditionalProcessing(&audioDataFloat)
// 保存处理后的数据
if status == noErr {
processor.audioListQueue.async {
processor.audioList.append(contentsOf: audioDataFloat)
}
}
return status
}
/// 读取处理后的音频数据
/// - Parameter bytes: 输出字节数组
/// - Returns: 读取的字节数
func read(bytes: inout [UInt8]) -> Int {
return audioListQueue.sync {
// 如果没有数据,返回0
if audioList.isEmpty {
return 0
}
// 确保有足够的数据 (至少1280个样本)
if audioList.count < 1280 {
return 0
}
// 读取一帧数据 (1280个样本)
let frameLength = 1280
let buffer = Array(audioList.prefix(frameLength))
audioList.removeFirst(frameLength)
// 将浮点数据转回Int16格式
var int16Data = buffer.map { Int16($0 * 32767) }
// 转换为字节数组
let data = Data(buffer: UnsafeBufferPointer(start: &int16Data, count: int16Data.count))
bytes = [UInt8](data)
// 每个样本2字节 (16位PCM)
return frameLength * 2
}
}
/// 检查错误并打印日志
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 是否发生错误
private func checkError(_ status: OSStatus, _ operation: String) -> Bool {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
return true
}
return false
}
/// 检查OSStatus并返回状态
/// - Parameters:
/// - status: 操作状态
/// - operation: 操作描述
/// - Returns: 原始状态
private func checkOSStatus(_ status: OSStatus, _ operation: String) -> OSStatus {
if status != noErr {
print("[CustomAudioProcessor] 错误: \(operation)失败: \(status)")
}
return status
}
}

18
azure/ios/Classes/AzureSpeechRecognitionPlugin.swift

@ -1,18 +0,0 @@
import Flutter
import UIKit
public class AzureSpeechRecognitionPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
if #available(iOS 13.0, *) {
SwiftAzureSpeechRecognitionPlugin.register(with: registrar)
} else {
// 如果低于iOS 13.0,返回不支持的错误
let channel = FlutterMethodChannel(name: "com.deep_voice.azure_asr", binaryMessenger: registrar.messenger())
channel.setMethodCallHandler { (call, result) in
result(FlutterError(code: "UNSUPPORTED",
message: "需要iOS 13.0及以上系统",
details: nil))
}
}
}
}

427
azure/ios/Classes/AzureTtsHelper.swift

@ -1,427 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
/// Azure TTS工具类,负责实现TTS服务接口
@available(iOS 13.0, *)
class AzureTtsHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (String, [String: Any]) -> Void
/// 语音配置信息
private var speechSubscriptionKey: String = ""
private var serviceRegion: String = ""
/// 语音合成配置
private var speechConfig: SPXSpeechConfiguration?
/// 语音合成器
private var synthesizer: SPXSpeechSynthesizer?
/// 是否初始化成功
private var isInitialized = false
/// 当前是否正在播放
private var _isSpeaking = false
/// 音频会话配置
private var isAudioSessionConfigured = false
// MARK: - 语音设置
/// 当前语音
private var currentVoice = "zh-CN-XiaoxiaoNeural"
/// 支持的语音映射
private var voiceMap: [String: String] = [
"zh-CN": "zh-CN-XiaoxiaoNeural",
"en-US": "en-US-JennyNeural",
"ja-JP": "ja-JP-NanamiNeural",
"ko-KR": "ko-KR-SunHiNeural",
"zh-TW": "zh-TW-HsiaoChenNeural",
"zh-HK": "zh-HK-HiuMaanNeural"
]
/// 当前语音合成参数
private var currentSpeechRate = "0%"
private var currentPitch = "0%"
private var currentVolume = "100%"
// MARK: - 初始化
init(eventHandler: @escaping (String, [String: Any]) -> Void) {
self.eventHandler = eventHandler
super.init()
}
deinit {
dispose()
}
// MARK: - TTS 接口实现
/// 初始化语音合成服务
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 服务区域 (如 eastasia)
/// - language: 语言代码 (默认 zh-CN)
/// - Returns: 初始化是否成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool {
print("[AzureTtsHelper] 初始化语音合成服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler("error", ["error": "Azure 配置信息不完整"])
return false
}
// 释放之前的资源
dispose()
// 记录配置信息
self.speechSubscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 配置音频会话
if !configureAudioSession() {
print("[AzureTtsHelper] 警告: 音频会话配置失败,将尝试继续初始化")
}
do {
// 创建语音配置
speechConfig = try SPXSpeechConfiguration(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置默认语音
let defaultVoice = getDefaultVoiceForLanguage(language)
currentVoice = defaultVoice
speechConfig?.speechSynthesisVoiceName = defaultVoice
// 创建语音合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig!)
// 设置事件处理器
setupSynthesizerEvents()
isInitialized = true
print("[AzureTtsHelper] TTS 引擎初始化成功")
return true
} catch {
print("[AzureTtsHelper] 错误: 初始化语音合成服务失败: \(error.localizedDescription)")
eventHandler("error", ["error": "初始化语音合成服务失败: \(error.localizedDescription)"])
return false
}
}
/// 配置音频会话
private func configureAudioSession() -> Bool {
let audioSession = AVAudioSession.sharedInstance()
do {
// 使用playback类别,但支持混合和空中播放
try audioSession.setCategory(.playback,
mode: .spokenAudio,
options: [.mixWithOthers, .allowAirPlay, .duckOthers])
// 根据设备类型选择最佳配置
let currentRoute = audioSession.currentRoute
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 优化音频路由
if hasHeadphones {
// 耳机模式,使用默认设置
try audioSession.setPreferredIOBufferDuration(0.005) // 较小的缓冲区大小以减少延迟
} else {
// 扬声器模式
try audioSession.setPreferredIOBufferDuration(0.005)
}
// 避免完全激活音频会话,因为ASR可能已经激活
// 这里使用setActive(false)是为了不与ASR冲突
if !audioSession.isOtherAudioPlaying {
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
}
isAudioSessionConfigured = true
print("[AzureTtsHelper] 音频会话配置成功")
return true
} catch {
print("[AzureTtsHelper] 警告: 音频会话配置失败: \(error.localizedDescription)")
isAudioSessionConfigured = false
return false
}
}
/// 设置语音
/// - Parameter voiceName: 语音名称 (如 "zh-CN-XiaoxiaoNeural")
/// - Returns: 设置是否成功
func setVoice(voiceName: String) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
if voiceName.isEmpty {
print("[AzureTtsHelper] 错误: 声音名称为空")
eventHandler("error", ["error": "声音名称不能为空"])
return false
}
if voiceName == currentVoice {
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
return true
}
print("[AzureTtsHelper] 设置声音: \(voiceName)")
currentVoice = voiceName
// 更新语音配置
if let speechConfig = speechConfig {
speechConfig.speechSynthesisVoiceName = voiceName
return true
}
return false
}
/// 设置语音合成参数
/// - Parameters:
/// - rate: 语速,范围 -100 到 100,默认为 0
/// - pitch: 音调,范围 -100 到 100,默认为 0
/// - volume: 音量,范围 0 到 100,默认为 100
/// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
// 转换参数格式
currentSpeechRate = formatRateParam(rate)
currentPitch = formatPitchParam(pitch)
currentVolume = formatVolumeParam(volume)
print("[AzureTtsHelper] 已设置语音参数: 语速=\(currentSpeechRate), 音调=\(currentPitch), 音量=\(currentVolume)")
return true
}
/// 合成文本为语音并播放
/// - Parameter text: 要合成的文本
/// - Returns: 操作是否成功启动
func speakText(text: String) -> Bool {
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler("error", ["error": "TTS 引擎尚未初始化"])
return false
}
if text.isEmpty {
print("[AzureTtsHelper] 警告: 要播放的文本为空")
return true
}
// 确保音频会话已配置
if !isAudioSessionConfigured {
_ = configureAudioSession()
}
print("[AzureTtsHelper] 开始语音合成: \(text.prefix(50))...")
// 生成SSML
let ssml = generateSsml(text: text)
// 直接进行SSML合成
return speakSsmlInternal(text: ssml)
}
/// 内部SSML合成和播放
private func speakSsmlInternal(text: String) -> Bool {
guard let synthesizer = synthesizer else {
print("[AzureTtsHelper] 错误: 合成器未初始化")
eventHandler("error", ["error": "合成器未初始化"])
return false
}
_isSpeaking = true
eventHandler("started", [:])
Task {
do {
// 使用异步方法进行合成并直接播放
_ = try await synthesizer.startSpeakingSsml(text)
} catch {
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成失败: \(error.localizedDescription)"])
}
}
}
return true
}
/// 停止当前语音合成
/// - Returns: 操作是否成功
func stopSpeaking() -> Bool {
if !isInitialized || !_isSpeaking {
return true
}
// 停止合成
do {
try synthesizer?.stopSpeaking()
_isSpeaking = false
eventHandler("canceled", [:])
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)")
eventHandler("error", ["error": "停止语音合成失败: \(error.localizedDescription)"])
return false
}
}
/// 检查是否正在播放
/// - Returns: 当前是否正在播放语音
func isSpeaking() -> Bool {
return _isSpeaking
}
/// 释放资源
func dispose() {
try? stopSpeaking()
// 释放合成器和配置
synthesizer = nil
speechConfig = nil
isInitialized = false
_isSpeaking = false
isAudioSessionConfigured = false
print("[AzureTtsHelper] TTS 引擎已释放")
}
// MARK: - 私有辅助方法
/// 设置合成器事件处理
private func setupSynthesizerEvents() {
guard let synthesizer = synthesizer else { return }
// 添加书签到达事件处理
synthesizer.addBookmarkReachedEventHandler { _, e in
print("[AzureTtsHelper] 书签事件: 音频偏移: \((e.audioOffset + 5000) / 10000)ms, 文本: \"\(e.text)\"")
}
// 合成完成事件
synthesizer.addSynthesisCompletedEventHandler { [weak self] _, e in
guard let self = self else { return }
print("[AzureTtsHelper] 语音合成完成: 音频持续时间: \(e.result.audioDuration)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("completed", [:])
}
}
// 合成取消事件
synthesizer.addSynthesisCanceledEventHandler { [weak self] _, e in
guard let self = self else { return }
let result = e.result
do {
let cancellationDetails = try SPXSpeechSynthesisCancellationDetails(fromCanceledSynthesisResult: result)
print("[AzureTtsHelper] 语音合成取消: 原因: \(cancellationDetails.reason)")
if cancellationDetails.reason == SPXCancellationReason.error {
print("[AzureTtsHelper] 错误代码: \(cancellationDetails.errorCode)")
print("[AzureTtsHelper] 错误详情: \(cancellationDetails.errorDetails ?? "未知")")
}
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
}
} catch {
print("[AzureTtsHelper] 获取取消详情时出错: \(error)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler("error", ["error": "语音合成被取消"])
}
}
}
// 合成开始事件
synthesizer.addSynthesisStartedEventHandler { _, _ in
// print("[AzureTtsHelper] 语音合成开始")
}
// 合成中事件
synthesizer.addSynthesizingEventHandler { _, _ in
// print("[AzureTtsHelper] 语音合成中")
}
}
/// 生成 SSML 文本
private func generateSsml(text: String) -> String {
return """
<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='zh-CN'>
<voice name='\(currentVoice)'>
<prosody rate='\(currentSpeechRate)' pitch='\(currentPitch)' volume='\(currentVolume)'>
\(text)
</prosody>
</voice>
</speak>
"""
}
/// 格式化语速参数
private func formatRateParam(_ rate: Int) -> String {
let clampedRate = rate.clamp(min: -100, max: 100)
if clampedRate == 0 {
return "0%"
} else if clampedRate < 0 {
return "\(Int(Double(clampedRate) * 0.9))%"
} else {
return "+\(clampedRate)%"
}
}
/// 格式化音调参数
private func formatPitchParam(_ pitch: Int) -> String {
let clampedPitch = pitch.clamp(min: -100, max: 100)
if clampedPitch == 0 {
return "0%"
} else {
return "\(Int(Double(clampedPitch) * 0.5))%"
}
}
/// 格式化音量参数
private func formatVolumeParam(_ volume: Int) -> String {
let clampedVolume = volume.clamp(min: 0, max: 100)
return "\(clampedVolume)%"
}
/// 获取指定语言的默认语音
private func getDefaultVoiceForLanguage(_ language: String) -> String {
return voiceMap[language] ?? "zh-CN-XiaoxiaoNeural"
}
}
// MARK: - 扩展
extension Int {
func clamp(min: Int, max: Int) -> Int {
if self < min { return min }
if self > max { return max }
return self
}
}

259
azure/ios/Classes/SwiftAzureSpeechRecognitionPlugin.swift

@ -1,259 +0,0 @@
import Flutter
import UIKit
import MicrosoftCognitiveServicesSpeech
import AVFoundation
@available(iOS 13.0, *)
public class SwiftAzureSpeechRecognitionPlugin: NSObject, FlutterPlugin {
private var azureChannel: FlutterMethodChannel
private var ttsChannel: FlutterMethodChannel
private var asrHelper: AzureAsrHelper
private var ttsHelper: AzureTtsHelper
private static var eventStreamHandler: AzureEventStreamHandler?
// 创建方法到通道的映射
private static var ttsMethodHandlers = [String: FlutterMethodCallHandler]()
private static var asrMethodHandlers = [String: FlutterMethodCallHandler]()
public static func register(with registrar: FlutterPluginRegistrar) {
// ASR通道
let channel = FlutterMethodChannel(name: "com.deep_voice.azure_asr", binaryMessenger: registrar.messenger())
// TTS通道
let ttsChannel = FlutterMethodChannel(name: "com.deep_voice.azure_tts", binaryMessenger: registrar.messenger())
// 设置ASR事件通道
let eventChannel = FlutterEventChannel(name: "com.deep_voice.azure_asr_events", binaryMessenger: registrar.messenger())
eventStreamHandler = AzureEventStreamHandler()
eventChannel.setStreamHandler(eventStreamHandler)
let instance = SwiftAzureSpeechRecognitionPlugin(
azureChannel: channel,
ttsChannel: ttsChannel,
eventStreamHandler: eventStreamHandler!
)
// 直接设置各自通道的处理器
channel.setMethodCallHandler(instance.handleAsrMethodCalls)
ttsChannel.setMethodCallHandler(instance.handleTtsMethodCalls)
}
// 新增直接处理方法调用的函数
private func handleTtsMethodCalls(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
handleTtsMethod(call, result)
}
private func handleAsrMethodCalls(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
handleAsrMethod(call, result)
}
init(azureChannel: FlutterMethodChannel, ttsChannel: FlutterMethodChannel, eventStreamHandler: AzureEventStreamHandler) {
self.azureChannel = azureChannel
self.ttsChannel = ttsChannel
// 创建辅助类实例,使用自定义事件回调处理器
let eventHandler: (String, [String: Any]) -> Void = { eventName, arguments in
DispatchQueue.main.async {
if let eventSink = SwiftAzureSpeechRecognitionPlugin.eventStreamHandler?.eventSink {
var eventData = arguments
eventData["type"] = eventName
eventSink(eventData)
}
}
}
asrHelper = AzureAsrHelper(eventHandler: eventHandler)
ttsHelper = AzureTtsHelper(eventHandler: eventHandler)
super.init()
}
private func handleAsrMethod(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let args = call.arguments as? Dictionary<String, Any>
switch call.method {
case "initialize":
// 仅在初始化时读取必要参数
guard let speechSubscriptionKey = args?["subscriptionKey"] as? String, !speechSubscriptionKey.isEmpty else {
let errorMsg = "语音订阅密钥不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SUBSCRIPTION_KEY", message: errorMsg, details: nil))
return
}
guard let serviceRegion = args?["region"] as? String, !serviceRegion.isEmpty else {
let errorMsg = "服务区域不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_REGION", message: errorMsg, details: nil))
return
}
let supportedLanguages = args?["supportedLanguages"] as? [String] ?? []
let success = asrHelper.initialize(
speechSubscriptionKey: speechSubscriptionKey,
serviceRegion: serviceRegion,
supportedLanguages: supportedLanguages.isEmpty ? nil : supportedLanguages
)
result(success)
case "startContinuousRecognition":
// 只有使用参数时才验证
let success = asrHelper.startContinuousRecognition()
result(success)
case "stopContinuousRecognition":
// 不需要额外参数
let success = asrHelper.stopContinuousRecognition()
result(success)
case "recognizeOnce":
// 只有使用参数时才验证
let success = asrHelper.recognizeOnce()
result(success)
case "isContinuousRecognitionActive":
// 不需要额外参数
result(asrHelper.isContinuousRecognitionActive())
case "dispose":
// 不需要额外参数
print("[AzurePlugin] 释放ASR资源")
asrHelper.dispose()
result(true)
default:
print("[AzurePlugin] 错误: 未知ASR方法: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
private func handleTtsMethod(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) {
let args = call.arguments as? Dictionary<String, Any>
switch call.method {
case "initialize":
// 仅在初始化时验证参数
guard let speechSubscriptionKey = args?["subscriptionKey"] as? String, !speechSubscriptionKey.isEmpty else {
let errorMsg = "语音订阅密钥不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SUBSCRIPTION_KEY", message: errorMsg, details: nil))
return
}
guard let serviceRegion = args?["region"] as? String, !serviceRegion.isEmpty else {
let errorMsg = "服务区域不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_REGION", message: errorMsg, details: nil))
return
}
let language = args?["language"] as? String ?? "zh-CN"
print("[AzurePlugin] 初始化TTS,语言: \(language)")
let success = ttsHelper.initialize(speechSubscriptionKey: speechSubscriptionKey, serviceRegion: serviceRegion, language: language)
result(success)
case "setVoice":
// 仅获取voice参数
guard let voiceName = args?["voiceName"] as? String, !voiceName.isEmpty else {
let errorMsg = "声音名称不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_VOICE", message: errorMsg, details: nil))
return
}
print("[AzurePlugin] 设置声音: \(voiceName)")
let success = ttsHelper.setVoice(voiceName: voiceName)
result(success)
case "speakText":
// 仅获取text参数
let text = args?["text"] as? String ?? ""
if text.isEmpty {
print("[AzurePlugin] 警告: 要播放的文本为空")
result("OK")
return
}
print("[AzurePlugin] 播放文本: \(text.prefix(50))...")
let success = ttsHelper.speakText(text: text)
result(success ? "OK" : "ERROR")
case "speakSsml":
// 仅获取ssml参数
guard let ssml = args?["ssml"] as? String, !ssml.isEmpty else {
let errorMsg = "SSML内容不能为空"
print("[AzurePlugin] 错误: \(errorMsg)")
result(FlutterError(code: "INVALID_SSML", message: errorMsg, details: nil))
return
}
print("[AzurePlugin] 播放SSML: \(ssml.prefix(100))...")
// 由于我们移除了speakSsml方法,这里改用speakText方法
// Azure SDK内部会自动检测是普通文本还是SSML
let success = ttsHelper.speakText(text: ssml)
result(success)
case "stopSpeaking":
// 不需要参数
print("[AzurePlugin] 停止播放")
let success = ttsHelper.stopSpeaking()
result(success)
case "isSpeaking":
// 不需要参数
result(ttsHelper.isSpeaking())
case "setSpeechParams":
// 仅获取语音参数
let rate = args?["rate"] as? Int ?? 0
let pitch = args?["pitch"] as? Int ?? 0
let volume = args?["volume"] as? Int ?? 100
print("[AzurePlugin] 设置语音参数: rate=\(rate), pitch=\(pitch), volume=\(volume)")
let success = ttsHelper.setSpeechParams(rate: rate, pitch: pitch, volume: volume)
result(success)
case "dispose":
// 释放TTS资源
print("[AzurePlugin] 释放TTS资源")
ttsHelper.dispose()
result(true)
default:
print("[AzurePlugin] 错误: 未知TTS方法: \(call.method)")
result(FlutterMethodNotImplemented)
}
}
}
// 用于处理事件流的辅助类
@available(iOS 13.0, *)
class AzureEventStreamHandler: NSObject, FlutterStreamHandler {
var eventSink: FlutterEventSink?
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
// 通知Flutter端事件通道已准备好
DispatchQueue.main.async {
events(["type": "channelReady"])
}
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}

24
azure/ios/azure_speech_recognition.podspec

@ -1,24 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint azure_speech_recognition.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'azure_speech_recognition'
s.version = '0.1.0'
s.summary = 'Azure Speech Recognition plugin for Flutter'
s.description = <<-DESC
A Flutter plugin for Microsoft Azure Speech services, providing both speech recognition (ASR) and text-to-speech (TTS) capabilities.
DESC
s.homepage = 'https://github.com/yourusername/azure_speech_recognition'
s.license = { :type => 'MIT', :file => '../LICENSE' }
s.author = { 'Your Company' => 'your-email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

6
azure/lib/azure_speech_recognition.dart

@ -1,6 +0,0 @@
// This is a placeholder file that exports nothing.
// The actual implementation is in the app's services folder.
// This file exists just to satisfy the Flutter plugin structure requirements.
// Empty library to satisfy plugin structure
library azure_speech_recognition;

23
azure/pubspec.yaml

@ -1,23 +0,0 @@
name: azure_speech_recognition
description: Azure Speech Recognition and Text-to-Speech services Flutter plugin
version: 0.1.0
homepage: https://github.com/yourusername/azure_speech_recognition
environment:
sdk: '>=2.12.0 <3.0.0'
flutter: ">=2.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.0
flutter:
plugin:
platforms:
ios:
pluginClass: AzureSpeechRecognitionPlugin

14
lib/core/bindings/initial_binding.dart

@ -8,7 +8,7 @@ import '../../data/services/classic_bluetooth_service.dart';
import '../../data/services/bluetooth_media_button_service.dart';
import '../../core/utils/logger.dart';
import '../../data/services/voice_interaction_service.dart';
import '../../data/services/open_ai_service_adapter.dart';
/// 初始绑定,用于管理全局依赖
class InitialBinding extends Bindings {
@override
@ -29,6 +29,13 @@ class InitialBinding extends Bindings {
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);
// OpenAI服务适配器
Get.lazyPut<OpenAIServiceAdapter>(() {
final adapter = OpenAIServiceAdapter();
adapter.initialize();
return adapter;
}, fenix: true);
// 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
@ -40,10 +47,15 @@ class InitialBinding extends Bindings {
Get.lazyPut<BluetoothMediaButtonService>(
() => BluetoothMediaButtonService(),
fenix: true);
// 语音交互服务
Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
fenix: true);
// 杰理蓝牙服务
// // 语言管理器(需要最先初始化)
// Get.put<LoginService>(LoginService(), permanent: true);
// await LoginService().FirebaseInit();

89
lib/core/utils/jieli_constants.dart

@ -0,0 +1,89 @@
/// 杰里(Jieli)设备相关常量定义
/// 用于标准化系统属性类型和功能码
/// 系统属性类型常量
class JieliAttrType {
// 公共属性
static const int VOLUME = 1; // 音量
static const int PLAYING_TIME = 2; // 播放时间
static const int BATTERY_LEVEL = 3; // 电池电量
static const int WORK_MODE = 4; // 工作模式
static const int PLAYBACK_MODE = 5; // 播放模式(循环模式)
static const int EQ_MODE = 6; // EQ模式
static const int LANGUAGE = 7; // 语言设置
static const int CONTRAST = 8; // 对比度
static const int LED_ONOFF = 9; // LED开关
static const int POWER_OFF_TIME = 10; // 关机时间
static const int LIGHT_ONOFF = 11; // 背光开关
static const int BLETYPE_SETTING = 12; // BLE类型设置
static const int TIME_OUT = 13; // 超时设置
static const int DEVICE_NAME = 14; // 设备名称
static const int ALARM_ENABLE = 15; // 闹钟开关
static const int DEV_MAC = 16; // 设备MAC地址
// 音乐相关属性
static const int MUSIC_STATUS = 20; // 音乐状态
static const int MUSIC_CURR_DEVICE = 21; // 当前音乐设备
static const int MUSIC_TOTAL_TIME = 22; // 音乐总时长
static const int MUSIC_CURR_PLAY_TIME = 23; // 当前播放时间
static const int MUSIC_NUM = 24; // 音乐数量
static const int MUSIC_DEV_STATUS = 25; // 设备存储状态
static const int MUSIC_BREAK_POINT = 26; // 断点信息
static const int MUSIC_CURR_FILENUM = 27; // 当前文件编号
static const int MUSIC_TOTAL_FILENUM = 28; // 总文件数
static const int MEDIA_TIMESTAMP = 29; // 媒体时间戳
// 模式相关
static const int CUR_MODE = 30; // 当前模式
// TWS相关
static const int TWS_CONN_STATUS = 60; // TWS连接状态
static const int LEFT_CHN_BAT = 61; // 左耳电量
static const int RIGHT_CHN_BAT = 62; // 右耳电量
static const int TWS_STATUS = 63; // TWS状态
}
/// 功能码
class JieliFunctionCode {
static const int COMMON = 0; // 公共功能
static const int MUSIC = 1; // 音乐功能
static const int RADIO = 2; // 收音机功能
static const int LINEIN = 3; // 线路输入功能
static const int RTC = 4; // 时钟功能
static const int BT = 5; // 蓝牙功能
static const int PC = 6; // PC功能
static const int RECORD = 7; // 录音功能
static const int UDISK = 8; // U盘功能
static const int TALK = 9; // 通话功能
static const int PHOTO = 10; // 拍照功能
static const int VIDEO = 11; // 视频功能
}
/// 音乐播放状态
class JieliMusicStatus {
static const int STOP = 0; // 停止状态
static const int PLAY = 1; // 播放状态
static const int PAUSE = 2; // 暂停状态
static const int FAST_FORWARD = 3; // 快进状态
static const int FAST_BACKWARD = 4; // 快退状态
}
/// 循环播放模式
class JieliPlaybackMode {
static const int ALL = 0; // 全部循环
static const int FOLDER = 1; // 文件夹循环
static const int ONE = 2; // 单曲循环
static const int RANDOM = 3; // 随机播放
static const int BROWSE = 4; // 浏览播放
}
/// EQ模式
class JieliEQMode {
static const int NORMAL = 0; // 正常
static const int ROCK = 1; // 摇滚
static const int POP = 2; // 流行
static const int CLASSIC = 3; // 经典
static const int JAZZ = 4; // 爵士
static const int COUNTRY = 5; // 乡村
static const int CUSTOM = 6; // 自定义
}

2
lib/core/widgets/message_bubble.dart

@ -52,7 +52,7 @@ class MessageBubble extends StatelessWidget {
),
// 加载指示器
if (message.isLoading)
if (message.isIntermediate)
Padding(
padding: EdgeInsets.only(top: message.content.isNotEmpty ? 8.h : 0),
child: _buildLoadingIndicator(),

13
lib/data/models/events/voice_interaction_event.dart

@ -26,4 +26,17 @@ class RecognitionStartedEvent extends VoiceInteractionEvent {
RecognitionStartedEvent({
required int timestamp,
}) : super(timestamp: timestamp);
}
/// 通用语音交互事件
/// 用于处理其他类型的事件
class GenericVoiceInteractionEvent extends VoiceInteractionEvent {
final String type;
final Map<String, dynamic>? data;
GenericVoiceInteractionEvent({
required this.type,
this.data,
required int timestamp,
}) : super(timestamp: timestamp);
}

2
lib/data/services/ai_service.dart

@ -1,5 +1,7 @@
/// AI回复服务接口
abstract class AiService {
/// 非流式输出方法
Future<String> sendMessage({
required List<Map<String, String>> messages,

105
lib/data/services/chat_history_service.dart

@ -1,16 +1,13 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../modules/chat/models/message_model.dart';
import '../../core/utils/logger.dart';
import 'package:chat_storage/chat_storage.dart';
/// 聊天记录管理服务,负责保存和加载聊天记录
class ChatHistoryService extends GetxService {
final GetStorage _storage = GetStorage();
// 存储键名
static const String _historyKeyPrefix = 'chat_history_';
static const int _maxHistoryCount = 10; // 每个对话最多保存的消息数
// 最大历史记录数量
static const int _maxHistoryCount = 200; // 每个对话最多保存的消息数
@override
void onInit() {
@ -18,50 +15,95 @@ class ChatHistoryService extends GetxService {
Logger.info('ChatHistoryService 已初始化');
}
/// 生成历史记录存储键
String _getHistoryKey(String agentId) => '$_historyKeyPrefix$agentId';
/// 保存聊天记录
///
/// [agentId] 代理ID
/// [messages] 消息列表
Future<void> saveHistory(String agentId, List<Message> messages) async {
try {
// 过滤掉加载中的消息
final filteredMessages = messages.where((m) => !m.isLoading).toList();
if (messages.isEmpty) return;
// 如果消息超过最大数量,只保留最新的消息
final messagesToSave = filteredMessages.length > _maxHistoryCount
? filteredMessages.sublist(filteredMessages.length - _maxHistoryCount)
: filteredMessages;
// 获取最新的消息
final latestMessage = messages.last;
// 转换为JSON
final jsonList = messagesToSave.map((m) => m.toJson()).toList();
final jsonString = jsonEncode(jsonList);
// 如果是加载中的消息则跳过
if (latestMessage.isIntermediate) return;
// 保存到存储
await _storage.write(_getHistoryKey(agentId), jsonString);
// Logger.info('已保存聊天记录,代理: $agentId,消息数: ${messagesToSave.length}');
// 直接插入最新的消息到数据库
await ChatStorage.saveMessage(
sessionId: agentId,
message: latestMessage.content,
sender: latestMessage.role,
);
// 检查并清理过多的历史记录
await _cleanupOldMessages(agentId);
} catch (e) {
Logger.error('保存聊天记录失败', e);
}
}
/// 清理旧消息,保持每个会话的消息数量不超过最大限制
Future<void> _cleanupOldMessages(String agentId) async {
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: 1, // 只获取总数信息
);
// 如果消息总数超过最大限制,删除最早的消息
if (pagedResult.totalCount > _maxHistoryCount) {
final messagesToDelete = pagedResult.totalCount - _maxHistoryCount;
// 获取最早的N条消息ID
final oldestMessages = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: messagesToDelete,
);
// 构建要删除的消息ID列表
final List<int> messageIds = oldestMessages.messages
.where((m) => m.id != null)
.map((m) => m.id!)
.toList();
// 删除这些消息
if (messageIds.isNotEmpty) {
await ChatStorage.deleteMessages(messageIds: messageIds);
}
}
} catch (e) {
Logger.error('清理旧消息失败', e);
}
}
/// 加载聊天记录
///
/// [agentId] 代理ID
/// 返回消息列表,如果没有找到或解析错误则返回空列表
List<Message> loadHistory(String agentId) {
Future<List<Message>> loadHistory(String agentId) async {
try {
final jsonString = _storage.read<String>(_getHistoryKey(agentId));
final pagedResult = await ChatStorage.getMessages(
sessionId: agentId,
page: 1,
pageSize: _maxHistoryCount,
);
if (jsonString == null) {
if (pagedResult.messages.isEmpty) {
return [];
}
final jsonList = jsonDecode(jsonString) as List;
final messages = jsonList.map((json) => Message.fromJson(json)).toList();
// 将ChatStorage.ChatMessage转换为Message
final messages = pagedResult.messages.map((chatMessage) {
return Message(
role: chatMessage.sender,
content: chatMessage.message,
timestamp: DateTime.parse(chatMessage.timestamp), // 直接使用chatMessage的timestamp
// isLoading 默认false
// isLoading: metadata['isLoading'] ?? false,
);
}).toList();
return messages;
} catch (e) {
@ -73,7 +115,7 @@ class ChatHistoryService extends GetxService {
/// 清除特定代理的聊天记录
Future<void> clearHistory(String agentId) async {
try {
await _storage.remove(_getHistoryKey(agentId));
await ChatStorage.deleteMessages(sessionId: agentId);
Logger.info('已清除聊天记录,代理: $agentId');
} catch (e) {
Logger.error('清除聊天记录失败', e);
@ -83,12 +125,7 @@ class ChatHistoryService extends GetxService {
/// 清除所有聊天记录
Future<void> clearAllHistory() async {
try {
final keys = _storage.getKeys();
for (final key in keys) {
if (key.startsWith(_historyKeyPrefix)) {
await _storage.remove(key);
}
}
await ChatStorage.clearDatabase();
Logger.info('已清除所有聊天记录');
} catch (e) {
Logger.error('清除所有聊天记录失败', e);

97
lib/data/services/function_service.dart

@ -1,97 +0,0 @@
import 'dart:convert';
/// 函数服务
/// 仅实现各种功能函数,不包含AI交互逻辑
class FunctionService {
/// 天气查询工具定义
/// 返回一个可直接用于tools参数的天气查询工具定义
static Map<String, dynamic> getWeatherToolDefinition() {
return {
'type': 'function',
'function': {
'name': 'get_current_weather',
'description': '获取指定地点的当前天气信息',
'parameters': {
'type': 'object',
'properties': {
'location': {
'type': 'string',
'description': '地理位置,例如:北京市',
},
'unit': {
'type': 'string',
'description': '温度单位',
'enum': ['celsius', 'fahrenheit'],
},
},
'required': ['location'],
},
},
};
}
/// 模拟天气API实现
/// 返回一个模拟的天气信息,实际应用中应该调用真实的天气API
static Future<Map<String, dynamic>> getWeatherInfo(Map<String, dynamic> args) async {
final location = args['location'] ?? '未知地点';
final unit = args['unit'] ?? 'celsius';
// 在实际应用中,这里应该调用真实的天气API
return {
'location': location,
'temperature': unit == 'celsius' ? '25°C' : '77°F',
'conditions': '晴转多云',
'humidity': '55%',
'wind': '西南风 3级',
'date': DateTime.now().toString().split(' ')[0],
};
}
/// 解析函数调用参数
/// 从AI响应中提取函数调用参数
static Map<String, dynamic> parseFunctionCallArguments(Map<String, dynamic> functionCallResponse) {
if (functionCallResponse['tool_calls'] == null ||
functionCallResponse['tool_calls'].isEmpty) {
throw FunctionServiceException('No tool calls found in response');
}
final toolCall = functionCallResponse['tool_calls'][0];
if (toolCall['function'] == null || toolCall['function']['arguments'] == null) {
throw FunctionServiceException('Invalid function call format');
}
try {
return json.decode(toolCall['function']['arguments']);
} catch (e) {
throw FunctionServiceException('Failed to parse function arguments: $e');
}
}
/// 获取函数调用ID和名称
/// 用于构建工具响应消息
static Map<String, String> getFunctionCallInfo(Map<String, dynamic> functionCallResponse) {
if (functionCallResponse['tool_calls'] == null ||
functionCallResponse['tool_calls'].isEmpty) {
throw FunctionServiceException('No tool calls found in response');
}
final toolCall = functionCallResponse['tool_calls'][0];
if (toolCall['id'] == null || toolCall['function'] == null || toolCall['function']['name'] == null) {
throw FunctionServiceException('Invalid function call format');
}
return {
'id': toolCall['id'],
'name': toolCall['function']['name'],
};
}
}
/// 函数调用服务异常类
class FunctionServiceException implements Exception {
final String message;
FunctionServiceException(this.message);
@override
String toString() => message;
}

278
lib/data/services/open_ai_service_adapter.dart

@ -0,0 +1,278 @@
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:open_ai_service/open_ai_service.dart';
import 'package:get/get.dart';
import 'ai_service.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
/// 定义流事件类型,用于区分不同类型的事件
enum StreamEventType { token, complete, error }
/// 流事件包装类
class StreamEvent {
final StreamEventType type;
final String? content;
StreamEvent(this.type, {this.content});
}
/// OpenAI服务适配器 - 连接AiService接口与OpenAIService插件
class OpenAIServiceAdapter implements AiService {
final OpenAIService _openAIService = OpenAIService();
StreamSubscription<OpenAIEvent>? _eventSubscription;
final StreamController<StreamEvent> _tokenStreamController =
StreamController<StreamEvent>.broadcast();
/// 构造函数
OpenAIServiceAdapter() {
printInfo(info: '创建OpenAIServiceAdapter实例');
_setupEventListener();
}
/// 设置事件监听器
void _setupEventListener() {
try {
// 首先确保访问eventStream以初始化底层事件通道
_openAIService.eventStream;
// 设置事件处理
_eventSubscription = _openAIService.eventStream.listen(
(event) {
try {
switch (event.type) {
case OpenAIEventType.token:
if (event.content is String) {
_tokenStreamController.add(StreamEvent(StreamEventType.token,
content: event.content as String));
} else {
printInfo(info: '收到非字符串类型的token: ${event.content}');
}
break;
case OpenAIEventType.complete:
_tokenStreamController
.add(StreamEvent(StreamEventType.complete));
break;
case OpenAIEventType.error:
_tokenStreamController.add(StreamEvent(StreamEventType.error,
content: '未知错误: ${event.content}'));
break;
case OpenAIEventType.functionCall:
printInfo(
info:
'接收到 functionCall content: ${event.content} meta: ${event.meta}');
// _tokenStreamController.add(StreamEvent(
// StreamEventType.error,
// content: '收到函数调用,该流仅支持文本响应'
// ));
break;
}
} catch (e) {
printError(info: '处理事件出错: $e');
_tokenStreamController
.add(StreamEvent(StreamEventType.error, content: '处理事件失败: $e'));
}
},
onError: (error) {
printError(info: '事件流错误: $error');
_tokenStreamController.add(
StreamEvent(StreamEventType.error, content: '事件流错误: $error'));
},
onDone: () {
printInfo(info: '事件流已关闭');
},
);
} catch (e) {
printError(info: '设置事件监听器失败: $e');
}
}
/// 初始化OpenAI服务
Future<bool> initialize() async {
try {
// 从.env文件中读取配置
final apiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
final baseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
final model = dotenv.env['OPENAI_MODEL'] ?? '';
printInfo(info: '从.env读取OpenAI配置');
printInfo(info: '基础URL: $baseUrl');
printInfo(info: '模型名称: $model');
if (apiKey.isEmpty) {
printError(info: '错误: OpenAI API密钥未配置,请在.env文件中设置OPENAI_API_KEY');
return false;
}
// 读取.mcp.json文件
String mcpConfig = '';
try {
// 从Flutter资源包中加载.mcp.json
mcpConfig = await rootBundle.loadString('.mcp.json');
printInfo(info: '成功从资源包加载.mcp.json配置文件');
// 验证JSON格式
final jsonData = jsonDecode(mcpConfig);
if (jsonData is Map && jsonData.containsKey('mcpServers')) {
printInfo(info: '解析到有效的mcpServers配置');
} else {
printInfo(info: '.mcp.json内容格式不正确,期望包含mcpServers字段');
}
} catch (e) {
printError(info: '加载.mcp.json资源文件时出错: $e');
}
// 初始化OpenAI服务
final result = await _openAIService.initialize(
apiKey: apiKey,
baseUrl: baseUrl,
model: model,
mcpServer: mcpConfig,
);
if (result) {
printInfo(info: 'OpenAI服务初始化成功');
} else {
printError(info: 'OpenAI服务初始化失败');
}
return result;
} catch (e) {
printError(info: 'OpenAI服务初始化异常: $e');
return false;
}
}
/// 发送消息并获取回复
@override
Future<String> sendMessage({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async {
try {
// 在方法内直接转换
final convertedMessages =
messages.map((m) => Map<String, dynamic>.from(m)).toList();
// 发送消息并获取回复
final response = await _openAIService.sendMessage(
messages: convertedMessages,
);
return response;
} catch (e) {
printError(info: 'OpenAI发送消息失败: $e');
throw '发送消息失败: $e';
}
}
/// 发送消息并获取流式回复
@override
Stream<String> sendMessageStream({
required List<Map<String, String>> messages,
required String systemPrompt,
}) async* {
try {
// 在方法内直接转换
final convertedMessages =
messages.map((m) => Map<String, dynamic>.from(m)).toList();
// 创建用于接收token的控制器
final localController = StreamController<String>();
// 添加从广播流到本地流的订阅
final subscription = _tokenStreamController.stream.listen((streamEvent) {
switch (streamEvent.type) {
case StreamEventType.token:
if (streamEvent.content != null && !localController.isClosed) {
localController.add(streamEvent.content!);
}
break;
case StreamEventType.complete:
if (!localController.isClosed) {
localController.close();
}
break;
case StreamEventType.error:
if (!localController.isClosed) {
printError(info: '令牌流错误: ${streamEvent.content}');
localController.addError(streamEvent.content ?? '未知错误');
localController.close();
}
break;
}
});
// 当本地控制器关闭时,取消订阅
localController.onCancel = () {
subscription.cancel();
};
// 启动流式消息请求
bool started = false;
try {
started = await _openAIService.sendMessageStream(
messages: convertedMessages,
);
} catch (e) {
printError(info: '启动消息流失败: $e');
if (!localController.isClosed) {
localController.addError('启动消息流失败: $e');
localController.close();
}
throw '启动消息流失败: $e';
}
if (!started) {
printError(info: '无法启动消息流');
if (!localController.isClosed) {
localController.addError('无法启动消息流');
localController.close();
}
throw '无法启动消息流';
}
// 通过yield*将controller的流转发
yield* localController.stream;
} catch (e) {
printError(info: 'OpenAI流式消息处理失败: $e');
throw '流式消息处理失败: $e';
}
}
/// 注册函数
Future<bool> registerFunction(
String name, String description, Map<String, dynamic> parameters) async {
try {
final result = await _openAIService.registerFunction(
name: name,
description: description,
parameters: parameters,
);
if (result) {
printInfo(info: '函数 "$name" 注册成功');
} else {
printError(info: '函数 "$name" 注册失败');
}
return result;
} catch (e) {
printError(info: '注册函数失败: $e');
return false;
}
}
/// 释放资源
void dispose() {
try {
_eventSubscription?.cancel();
_tokenStreamController.close();
printInfo(info: 'OpenAIServiceAdapter资源已释放');
} catch (e) {
printError(info: '释放资源时出错: $e');
}
}
}

4
lib/data/services/speech_impl/azure_asr_service.dart

@ -10,8 +10,8 @@ import '../asr_service.dart';
/// 该服务提供了通过平台通道与 Android 上的 Microsoft Speech SDK 交互的接口
class AzureAsrService extends GetxService implements AsrService {
static final AzureAsrService to = Get.put(AzureAsrService());
static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_asr');
static const EventChannel _eventChannel = EventChannel('com.deep_voice.azure_asr_events');
static const MethodChannel _channel = MethodChannel('azure_speech/asr');
static const EventChannel _eventChannel = EventChannel('azure_speech/asr_events');
bool _isInitialized = false;
late final String _subscriptionKey;

2
lib/data/services/speech_impl/azure_tts_service.dart

@ -12,7 +12,7 @@ import '../tts_service.dart';
/// 提供文本转语音功能。
class AzureTtsService extends GetxService implements TtsService {
static final AzureTtsService to = Get.put(AzureTtsService());
static const MethodChannel _channel = MethodChannel('com.deep_voice.azure_tts');
static const MethodChannel _channel = MethodChannel('azure_speech/tts');
bool _isInitialized = false;
late final String _subscriptionKey;

15
lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart

@ -267,12 +267,12 @@ class AndroidVoiceInteractionService extends GetxService {
}
/// 保存聊天记录
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) async {
try {
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
if (userMessage.isEmpty) {
return;
}
// 创建用户消息和助手消息
final userMsg = Message(
role: 'user',
content: userMessage,
@ -285,14 +285,17 @@ class AndroidVoiceInteractionService extends GetxService {
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp + 1), // 确保助手消息时间戳晚于用户消息
);
// 获取ChatHistoryService实例
final chatHistoryService = Get.find<ChatHistoryService>();
// 加载现有历史记录
final existingMessages = chatHistoryService.loadHistory(agentId);
final existingMessages = await chatHistoryService.loadHistory(agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
chatHistoryService.saveHistory(agentId, existingMessages);
await chatHistoryService.saveHistory(agentId, existingMessages);
} catch (e) {
Logger.error('保存聊天记录失败: $e');
}

8
lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart

@ -250,7 +250,7 @@ class IosVoiceInteractionService extends GetxService {
);
// 保存聊天历史
_saveChatHistory(userMsg, assistantMsg);
await _saveChatHistory(userMsg, assistantMsg);
// 发送聊天历史事件
_sendChatHistoryEvent(userMsg.content, assistantMsg.content);
@ -424,16 +424,16 @@ class IosVoiceInteractionService extends GetxService {
}
// 保存聊天历史
void _saveChatHistory(Message userMsg, Message assistantMsg) {
Future<void> _saveChatHistory(Message userMsg, Message assistantMsg) async {
try {
// 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(_agentId);
final existingMessages = await _chatHistoryService.loadHistory(_agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(_agentId, existingMessages);
await _chatHistoryService.saveHistory(_agentId, existingMessages);
Logger.info('保存聊天历史记录成功');
} catch (e) {

85
lib/data/services/voice_interaction_service.dart

@ -1,10 +1,12 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../models/events/voice_interaction_event.dart';
import '../../core/utils/logger.dart';
import '../../modules/chat/models/message_model.dart';
import '../../routes/app_routes.dart';
import 'chat_history_service.dart';
/// 语音交互服务接口
@ -38,7 +40,13 @@ class VoiceInteractionService extends GetxService {
// 配置信息
late String _azureSpeechKey;
late String _azureSpeechRegion;
late String _volcanoAiKey;
late String _openaiApiKey;
late String _openaiBaseUrl;
late String _openaiModel;
late String _volcanoSpeechAppId;
late String _volcanoSpeechAppToken;
// MCP配置信息
String? _mcpConfig;
// 聊天历史服务
late final ChatHistoryService _chatHistoryService;
@ -57,14 +65,30 @@ class VoiceInteractionService extends GetxService {
void _loadConfig() {
_azureSpeechKey = dotenv.env['AZURE_SPEECH_KEY'] ?? '';
_azureSpeechRegion = dotenv.env['AZURE_SPEECH_REGION'] ?? '';
_volcanoAiKey = dotenv.env['VOLCANO_AI_API_KEY'] ?? '';
_openaiApiKey = dotenv.env['OPENAI_API_KEY'] ?? '';
_openaiBaseUrl = dotenv.env['OPENAI_BASE_URL'] ?? '';
_openaiModel = dotenv.env['OPENAI_MODEL'] ?? '';
_volcanoSpeechAppId = dotenv.env['VOLCANO_SPEECH_APP_ID'] ?? '';
_volcanoSpeechAppToken = dotenv.env['VOLCANO_SPEECH_APP_TOKEN'] ?? '';
if (_azureSpeechKey.isEmpty || _azureSpeechRegion.isEmpty) {
Logger.warning('未找到 Azure 语音服务配置。请在 .env 文件中设置 AZURE_SPEECH_KEY 和 AZURE_SPEECH_REGION');
}
if (_volcanoAiKey.isEmpty) {
Logger.warning('未找到火山 AI API 密钥。请在 .env 文件中设置 VOLCANO_AI_API_KEY');
// 加载MCP配置
_loadMcpConfig();
}
/// 加载MCP配置
void _loadMcpConfig() {
try {
rootBundle.loadString('.mcp.json').then((config) {
_mcpConfig = config;
Logger.info('成功加载MCP配置, 配置内容: $_mcpConfig');
});
} catch (e) {
Logger.warning('加载MCP配置失败: $e');
_mcpConfig = null;
}
}
@ -102,18 +126,34 @@ class VoiceInteractionService extends GetxService {
_eventStreamController.add(chatHistoryEvent);
// 保存聊天历史到ChatHistoryService
_saveChatHistory(agentId, userMessage, assistantMessage, DateTime.now().millisecondsSinceEpoch);
_saveChatHistory(agentId, userMessage, assistantMessage);
break;
case 'enter_translation_mode':
// 进入翻译模式事件
Logger.info('收到进入翻译模式事件,正在导航到翻译界面');
_navigateToTranslation();
break;
}
}
/// 导航到翻译界面
void _navigateToTranslation() {
try {
// 使用GetX导航到翻译页面
Get.offNamedUntil(Routes.translation, (route) => route.settings.name == Routes.home);
Logger.info('已导航到翻译界面');
} catch (e) {
Logger.error('导航到翻译界面失败: $e');
}
}
/// 保存聊天历史
void _saveChatHistory(String agentId, String userMessage, String assistantMessage, int timestamp) {
Future<void> _saveChatHistory(String agentId, String userMessage, String assistantMessage) async {
try {
// 检查参数有效性
if (userMessage.isEmpty) {
return;
}
final timestamp = DateTime.now().millisecondsSinceEpoch;
// 创建用户消息和助手消息
final userMsg = Message(
@ -129,13 +169,13 @@ class VoiceInteractionService extends GetxService {
);
// 加载现有历史记录
final existingMessages = _chatHistoryService.loadHistory(agentId);
final existingMessages = await _chatHistoryService.loadHistory(agentId);
// 添加新消息
existingMessages.addAll([userMsg, assistantMsg]);
// 保存更新后的历史记录
_chatHistoryService.saveHistory(agentId, existingMessages);
await _chatHistoryService.saveHistory(agentId, existingMessages);
Logger.info('已保存聊天历史: agentId=$agentId');
} catch (e) {
@ -191,12 +231,23 @@ class VoiceInteractionService extends GetxService {
try {
Logger.info('启动语音交互服务...');
// 使用已加载的配置信息
final result = await _channel.invokeMethod<bool>('startService', {
// 构建参数
final Map<String, dynamic> params = {
'azure_speech_key': _azureSpeechKey,
'azure_speech_region': _azureSpeechRegion,
'volcano_ai_api_key': _volcanoAiKey,
}) ?? false;
'openai_api_key': _openaiApiKey,
'openai_base_url': _openaiBaseUrl,
'openai_model': _openaiModel,
'volcano_speech_app_id': _volcanoSpeechAppId,
'volcano_speech_app_token': _volcanoSpeechAppToken,
};
// 如果有MCP配置,添加到参数中
if (_mcpConfig != null) {
params['mcp_config'] = _mcpConfig;
}
final result = await _channel.invokeMethod<bool>('startService', params) ?? false;
if (result) {
_isServiceRunning.value = true;

315
lib/modules/chat/controllers/chat_controller.dart

@ -3,27 +3,26 @@ import 'package:get/get.dart';
import 'package:flutter/widgets.dart';
import 'package:get_storage/get_storage.dart';
import 'package:just_audio/just_audio.dart';
import 'package:chat_storage/chat_storage.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/deapsound_ai_service.dart';
import '../../../data/services/kouzi_ai_service.dart';
import '../../../data/services/speech_impl/voice_clone_tts_service.dart';
import '../models/message_model.dart';
import '../../../core/utils/logger.dart';
import '../../../data/services/volcano_ai_service.dart';
import '../../../data/services/tts_service.dart';
import '../../../data/providers/agent_provider.dart';
import '../../../data/models/agent.dart';
import '../../../data/services/asr_service.dart';
import '../../../data/services/chat_history_service.dart';
import '../../../data/models/events/voice_interaction_event.dart';
import '../../../data/services/voice_interaction_service.dart';
import '../../../data/services/open_ai_service_adapter.dart';
class ChatController extends GetxController {
// 服务
late AiService _aiService;
late TtsService _ttsService;
final AsrService _asrService = Get.find<AsrService>();
final ChatHistoryService _historyService = Get.find<ChatHistoryService>();
final VoiceInteractionService _voiceInteractionService =
Get.find<VoiceInteractionService>();
@ -45,6 +44,13 @@ class ChatController extends GetxController {
final RxBool isTtsEnabled = true.obs;
final RxBool isVoiceRecognitionActive = false.obs;
// 分页加载状态
final RxInt _currentPage = 1.obs;
final RxInt _totalPages = 1.obs;
final RxBool _isLoadingMore = false.obs;
final RxBool _hasMoreHistory = true.obs;
static const int _pageSize = 20; // 每页加载的消息数量
// 语音交互模式
bool _isVoiceInteractionMode = false;
@ -53,9 +59,6 @@ class ChatController extends GetxController {
final RxBool isListening = false.obs;
final RxBool isSpeechDetected = false.obs;
// 临时消息索引,用于更新临时消息
int _tempMessageIndex = -1;
// 防抖计时器
Timer? _scrollDebounceTimer;
@ -97,6 +100,7 @@ class ChatController extends GetxController {
agent = foundAgent;
// 根据Agent ID选择不同的AI服务
switch (agent.id) {
case 'cyber_girlfriend':
// 亲子陪伴
@ -109,7 +113,9 @@ class ChatController extends GetxController {
_aiService = Get.find<DeapsoundAIService>();
break;
default:
_aiService = Get.find<VolcanoAIService>();
// 默认使用OpenAIServiceAdapter
_aiService = Get.find<OpenAIServiceAdapter>();
Logger.info('使用OpenAIServiceAdapter');
}
// 使用克隆音色语音合成
@ -131,8 +137,11 @@ class ChatController extends GetxController {
// 设置TTS服务
await _ttsService.setVoice(agent.voice);
// 加载聊天历史
_loadChatHistory();
// 添加滚动监听器,用于触发加载更多
scrollController.addListener(_scrollListener);
// 加载聊天历史 (加载第一页)
await _loadChatHistory();
// 如果没有历史记录,添加欢迎消息
if (messages.isEmpty && agent.welcomeMessage.isNotEmpty) {
@ -216,7 +225,9 @@ class ChatController extends GetxController {
messages.add(userMsg);
messages.add(assistantMsg);
}
_scrollToBottom();
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
/// 停止当前交互
@ -242,6 +253,7 @@ class ChatController extends GetxController {
Logger.info('ChatController onClose');
// 清理资源
scrollController.removeListener(_scrollListener); // 移除监听器
scrollController.dispose();
_aiResponseSubscription?.cancel();
_recognitionSubscription?.cancel();
@ -258,7 +270,7 @@ class ChatController extends GetxController {
// 保存当前聊天记录
if (agent.id != null) {
_historyService.saveHistory(agent.id!, messages.toList());
// _historyService.saveHistory(agent.id!, messages.toList());
}
player.dispose();
@ -294,11 +306,12 @@ class ChatController extends GetxController {
_aiResponseSubscription!.cancel();
_aiResponseSubscription = null;
// 将加载中的消息转换为正常消息,保留已输出的内容
final index = messages.indexWhere((m) => m.isLoading);
// 将AI中间消息转换为正常消息,保留已输出的内容
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
messages[index] = messages[index].copyWith(
isLoading: false,
isIntermediate: false,
);
messages.refresh();
}
@ -328,10 +341,10 @@ class ChatController extends GetxController {
// 清空输入框
inputText.value = '';
// 添加加载中的消息
final loadingMessage = Message.loading();
messages.add(loadingMessage);
// 添加中间状态的AI消息
final intermediateMessage = Message.intermediate('assistant');
messages.add(intermediateMessage);
messages.refresh();
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
@ -359,15 +372,15 @@ class ChatController extends GetxController {
if (_shouldCancelAiResponse) return;
if (chunk is Map) {
_updateLoadingMessage(data: chunk);
_updateIntermediateMessage(data: chunk);
if (chunk['musicaudio'] != null) {
play(chunk['musicaudio']);
}
} else {
fullResponse += chunk;
// 更新加载中的消息内容
_updateLoadingMessage(content: fullResponse);
// 更新中间状态的消息内容
_updateIntermediateMessage(content: fullResponse);
// 如果TTS启用,使用流式处理发送到TTS
if (isTtsEnabled.value) {
@ -378,21 +391,18 @@ class ChatController extends GetxController {
onDone: () {
if (_shouldCancelAiResponse) return;
// 完成响应,将isLoading设为false
// 完成响应,将isIntermediate设为false
_finalizeResponse(fullResponse);
// 如果TTS启用,处理缓冲区中的剩余文本
if (isTtsEnabled.value) {
_ttsService.flushStream();
}
// 保存聊天历史
_saveChatHistory();
},
onError: (error) {
Logger.error('AI响应错误', error);
// 更新加载中的消息为错误消息
// 更新中间状态的消息为错误消息
_handleResponseError();
},
);
@ -408,7 +418,7 @@ class ChatController extends GetxController {
final List<Map<String, String>> messageHistory = [];
// 只取最近的10条消息作为上下文
final recentMessages = messages.where((m) => !m.isLoading).toList();
final recentMessages = messages.where((m) => !m.isIntermediate).toList();
final startIndex =
recentMessages.length > 10 ? recentMessages.length - 10 : 0;
@ -423,23 +433,20 @@ class ChatController extends GetxController {
return messageHistory;
}
// 更新加载中的消息
void _updateLoadingMessage({Map? data, String? content}) {
final index = messages.indexWhere((m) => m.isLoading);
// 更新中间状态的消息
void _updateIntermediateMessage({Map? data, String? content}) {
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
if (data != null) {
Map messagesData = messages[index].data ?? {};
messages[index] = messages[index].copyWith(
data: {...messagesData, ...data},
isLoading: true,
);
}
if (content != null) {
messages[index] = messages[index].copyWith(
content: content,
isLoading: true,
);
}
messages[index] = messages[index].copyWith(
content: content,
);
messages.refresh();
_scrollToBottom();
}
@ -469,29 +476,51 @@ class ChatController extends GetxController {
isPlay.value = false;
}
// 完成响应
// 更新或添加最终响应
void _finalizeResponse(String content) {
final index = messages.indexWhere((m) => m.isLoading);
// 查找AI的中间状态消息
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
// 更新中间状态消息为最终状态
messages[index] = messages[index].copyWith(
content: content,
isLoading: false,
isIntermediate: false,
timestamp: DateTime.now(), // 更新时间戳为最终时间
);
messages.refresh();
_scrollToBottom();
// 保存最终的助手消息到数据库
_saveMessageToDb(messages[index]);
} else {
// 如果没有中间状态消息,直接添加新消息 (理论上不应发生)
final assistantMsg = Message(
role: 'assistant',
content: content,
timestamp: DateTime.now(),
);
messages.add(assistantMsg);
_saveMessageToDb(assistantMsg); // 保存到数据库
}
isLoading.value = false;
isSending.value = false;
_aiResponseSubscription = null;
_aiResponseSubscription = null; // 清理订阅
_shouldCancelAiResponse = false; // 重置取消标志
// 响应完成后,重新启动语音识别的自动停止计时器
if (_isVoiceInteractionMode) {
_startAutoStopTimer();
}
}
// 处理响应错误
void _handleResponseError() {
final index = messages.indexWhere((m) => m.isLoading);
// 查找AI的中间状态消息
final index =
messages.indexWhere((m) => m.isIntermediate && m.role == 'assistant');
if (index != -1) {
messages[index] = messages[index].copyWith(
content: '抱歉,我遇到了一些问题,无法回应您的请求。',
isLoading: false,
isIntermediate: false,
);
messages.refresh();
}
@ -524,9 +553,6 @@ class ChatController extends GetxController {
isListening.value = true;
recognizedText.value = '';
// 重置临时消息索引
_tempMessageIndex = -1;
// 启动自动停止计时器
_startAutoStopTimer();
@ -618,24 +644,24 @@ class ChatController extends GetxController {
_interruptCurrentResponse("检测到用户开始讲话,中断当前响应");
}
// 更新或创建临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
// 更新已有的临时消息
messages[_tempMessageIndex] = messages[_tempMessageIndex].copyWith(
// 查找或创建用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
// 更新已有的中间状态消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
content: text,
);
messages.refresh();
} else {
// 添加新的临时消息
final tempMessage = Message(
role: 'user',
content: text,
timestamp: DateTime.now(),
);
messages.add(tempMessage);
_tempMessageIndex = messages.length - 1;
// 添加新的中间状态消息
var intermediateMessage = Message.intermediate('user');
intermediateMessage = intermediateMessage.copyWith(content: text);
messages.add(intermediateMessage);
messages.refresh();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
@ -651,14 +677,13 @@ class ChatController extends GetxController {
// 更新活动时间
_updateActivityTime();
// 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
messages.removeAt(_tempMessageIndex);
// 查找并移除用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
messages.removeAt(intermediateIndex);
}
// 重置临时消息索引
_tempMessageIndex = -1;
// 发送识别到的文本
sendMessage(text);
@ -685,16 +710,21 @@ class ChatController extends GetxController {
// 清理语音识别资源
void _cleanupRecognition() {
// 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
if (messages[_tempMessageIndex].content.trim().isEmpty) {
messages.removeAt(_tempMessageIndex);
// 查找并移除用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
if (messages[intermediateIndex].content.trim().isEmpty) {
messages.removeAt(intermediateIndex);
} else {
// 如果中间状态消息有内容,将其转换为正式消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
isIntermediate: false,
);
}
messages.refresh();
}
// 重置临时消息索引
_tempMessageIndex = -1;
// 停止语音识别
isVoiceRecognitionActive.value = false;
isListening.value = false;
@ -716,15 +746,21 @@ class ChatController extends GetxController {
recognizedText.value = '';
isSpeechDetected.value = false;
// 查找并移除临时消息
if (_tempMessageIndex >= 0 && _tempMessageIndex < messages.length) {
if (messages[_tempMessageIndex].content.trim().isEmpty) {
messages.removeAt(_tempMessageIndex);
// 查找并处理用户的中间状态消息
final intermediateIndex =
messages.indexWhere((m) => m.isIntermediate && m.role == 'user');
if (intermediateIndex >= 0) {
if (messages[intermediateIndex].content.trim().isEmpty) {
// 如果没有内容则移除
messages.removeAt(intermediateIndex);
} else {
// 有内容则转为正式消息
messages[intermediateIndex] = messages[intermediateIndex].copyWith(
isIntermediate: false,
);
}
messages.refresh();
}
// 重置临时消息索引
_tempMessageIndex = -1;
} catch (e) {
Logger.error('停止语音识别失败', e);
}
@ -733,7 +769,7 @@ class ChatController extends GetxController {
// 清空聊天记录
void clearChat() {
messages.clear();
_ttsService.stop();
// 添加欢迎消息
if (agent.welcomeMessage.isNotEmpty) {
messages.add(Message(
@ -742,31 +778,118 @@ class ChatController extends GetxController {
timestamp: DateTime.now(),
));
}
// 删除数据库中的历史记录
ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) {
Logger.error('清空数据库聊天记录失败: $e');
});
}
// 删除存储的历史记录
_historyService.clearHistory(agent.id);
// 滚动监听,用于触发加载更多历史记录
void _scrollListener() {
// 当滚动到顶部并且有更多历史记录且不在加载中时,加载更多
if (scrollController.position.pixels ==
scrollController.position.minScrollExtent &&
_hasMoreHistory.value &&
!_isLoadingMore.value) {
_loadMoreHistory();
}
}
// 加载聊天历史
void _loadChatHistory() {
// 从历史服务加载
final history = _historyService.loadHistory(agent.id);
if (history.isNotEmpty) {
messages.clear();
messages.addAll(history);
// 加载聊天历史 (首次加载或刷新)
Future<void> _loadChatHistory() async {
isLoading.value = true;
_currentPage.value = 1; // 重置为第一页
_hasMoreHistory.value = true; // 假设有更多
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agent.id,
page: _currentPage.value,
pageSize: _pageSize,
);
_totalPages.value = pagedResult.totalPages;
_hasMoreHistory.value = _currentPage.value < _totalPages.value;
final historyMessages = pagedResult.messages.map((chatMsg) {
return Message(
role: chatMsg.sender,
content: chatMsg.message,
timestamp: DateTime.parse(chatMsg.timestamp),
);
}).toList();
messages.assignAll(historyMessages.reversed); // 最新的在底部,所以要反转
// 滚动到底部 (如果是首次加载)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
scrollController.jumpTo(scrollController.position.maxScrollExtent);
}
});
} catch (e) {
Logger.error('加载聊天历史失败', e);
} finally {
isLoading.value = false;
}
}
// 保存聊天历史
void _saveChatHistory() {
_historyService.saveHistory(agent.id, messages);
// 加载更多历史记录
Future<void> _loadMoreHistory() async {
if (_isLoadingMore.value || !_hasMoreHistory.value) return;
_isLoadingMore.value = true;
_currentPage.value++;
try {
final pagedResult = await ChatStorage.getMessages(
sessionId: agent.id,
page: _currentPage.value,
pageSize: _pageSize,
);
_totalPages.value = pagedResult.totalPages;
_hasMoreHistory.value = _currentPage.value < _totalPages.value;
final olderMessages = pagedResult.messages.map((chatMsg) {
return Message(
role: chatMsg.sender,
content: chatMsg.message,
timestamp: DateTime.parse(chatMsg.timestamp),
);
}).toList();
// 将旧消息插入到列表顶部
messages.insertAll(0, olderMessages.reversed);
} catch (e) {
Logger.error('加载更多聊天历史失败', e);
_currentPage.value--; // 加载失败,恢复页码
} finally {
_isLoadingMore.value = false;
}
}
// 添加消息,供外部调用
// 保存单条聊天消息到数据库
Future<void> _saveMessageToDb(Message message) async {
if (message.isIntermediate) return; // 不保存中间状态的消息
try {
await ChatStorage.saveMessage(
sessionId: agent.id,
message: message.content,
sender: message.role,
);
} catch (e) {
Logger.error('保存消息到数据库失败', e);
}
}
// 添加消息,供外部调用 (如用户发送消息、AI回复)
void addMessage(Message message) {
messages.add(message);
_saveChatHistory();
_scrollToBottom();
_saveMessageToDb(message); // 保存到数据库
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToBottom();
});
}
void _stopVoiceRecognitionAutomatically() {

20
lib/modules/chat/models/message_model.dart

@ -3,14 +3,14 @@ class Message {
final String content;
final Map? data;
final DateTime timestamp;
final bool isLoading;
final bool isIntermediate; // 中间状态标记,替代isLoading
Message({
required this.role,
required this.content,
required this.timestamp,
this.data,
this.isLoading = false,
this.isIntermediate = false, // 默认不是中间状态
});
// 从JSON构造函数
@ -20,7 +20,7 @@ class Message {
content: json['content'] as String,
data: json['data'],
timestamp: DateTime.parse(json['timestamp'] as String),
isLoading: json['isLoading'] as bool? ?? false,
isIntermediate: json['isIntermediate'] as bool? ?? false,
);
}
@ -31,17 +31,17 @@ class Message {
'content': content,
'data': data,
'timestamp': timestamp.toIso8601String(),
'isLoading': isLoading,
'isIntermediate': isIntermediate,
};
}
// 创建一个加载中的消息
factory Message.loading() {
// 创建一个中间状态的消息
factory Message.intermediate(String role) {
return Message(
role: 'assistant',
role: role,
content: '',
timestamp: DateTime.now(),
isLoading: true,
isIntermediate: true,
);
}
@ -51,14 +51,14 @@ class Message {
String? content,
Map? data,
DateTime? timestamp,
bool? isLoading,
bool? isIntermediate,
}) {
return Message(
role: role ?? this.role,
content: content ?? this.content,
data: data ?? this.data,
timestamp: timestamp ?? this.timestamp,
isLoading: isLoading ?? this.isLoading,
isIntermediate: isIntermediate ?? this.isIntermediate,
);
}
}

47
lib/modules/chat/views/chat_navigation_card.dart

@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:math' as math;
import 'package:amap_flutter/amap_flutter.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
@ -20,10 +22,41 @@ class _ChatNavigationCardState extends State<ChatNavigationCard> {
LatLng startPoint = LatLng(116.397428, 39.90923); // 起点坐标
LatLng endPoint = LatLng(116.397026, 39.903303); // 终点坐标
final _amapFlutter = AmapFlutter();
StreamSubscription? _locationStream;
String _mapUrl = '';
@override
void initState() {
super.initState();
_getMapUrl();
_getLocation();
}
@override
void dispose() {
_amapFlutter.dispose();
_locationStream?.cancel();
super.dispose();
}
void _getLocation() async {
List endPointList = widget.data?['navigationcard']['location'].split(',');
endPoint = LatLng(
double.parse(endPointList[0]),
double.parse(endPointList[1]),
);
await _amapFlutter.initLocation();
_locationStream = _amapFlutter.locationStream().listen((data) async {
startPoint = LatLng(
data['longitude'],
data['latitude'],
);
_mapUrl = await _getMapUrl();
_amapFlutter.stopLocation();
setState(() {});
});
_amapFlutter.startLocation();
}
Future<String> _getMapUrl() async {
@ -157,15 +190,9 @@ class _ChatNavigationCardState extends State<ChatNavigationCard> {
Widget build(BuildContext context) {
return Column(
children: [
FutureBuilder(
future: _getMapUrl(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
return Image.network(snapshot.data);
}
return const CircularProgressIndicator();
},
),
_mapUrl.isNotEmpty
? Image.network(_mapUrl)
: const CircularProgressIndicator(),
],
);
}

2
lib/modules/chat/views/chat_view.dart

@ -303,7 +303,7 @@ class ChatView extends GetView<ChatController> {
_buildMessageContent(message, isDarkMode, isUser),
// 加载中动画
if (message.isLoading)
if (message.isIntermediate)
Padding(
padding: EdgeInsets.only(top: 8.h),
child: Row(

11
lib/modules/jieli_test/bindings/jieli_test_binding.dart

@ -0,0 +1,11 @@
import 'package:get/get.dart';
import '../controllers/jieli_test_controller.dart';
class JieliTestBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<JieliTestController>(
() => JieliTestController(),
);
}
}

323
lib/modules/jieli_test/controllers/jieli_test_controller.dart

@ -0,0 +1,323 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart' hide ConnectionState;
import 'package:get/get.dart';
import 'package:jieli/jieli.dart';
/// 杰理测试控制器
class JieliTestController extends GetxController {
// Jieli插件实例
late Jieli jieli;
// 状态变量
var isScanning = false.obs;
var deviceInfo = '未获取'.obs;
var connectStatus = '未连接'.obs;
var currentDevice = Rx<BleDevice?>(null);
var scannedDevices = RxList<BleDevice>([]);
var wakeupReceived = false.obs;
var audioDataSize = 0.obs;
var receivedFrames = 0.obs;
var audioDataReceiving = false.obs;
var volumeCommandResult = ''.obs;
// 事件订阅
StreamSubscription? _scanSubscription;
StreamSubscription? _connectionSubscription;
StreamSubscription? _wakeupSubscription;
StreamSubscription? _audioSubscription;
StreamSubscription? _commandResultSubscription;
@override
void onInit() {
super.onInit();
_initializeJieli();
}
@override
void onClose() {
_cancelAllSubscriptions();
_disposeJieli();
super.onClose();
}
/// 取消所有订阅
void _cancelAllSubscriptions() {
_scanSubscription?.cancel();
_connectionSubscription?.cancel();
_wakeupSubscription?.cancel();
_audioSubscription?.cancel();
_commandResultSubscription?.cancel();
}
/// 初始化杰理SDK
void _initializeJieli() {
try {
jieli = Jieli();
_setupEventListeners();
} catch (e) {
// 初始化错误处理
}
}
/// 设置事件监听器
void _setupEventListeners() {
// 监听扫描结果
_scanSubscription = jieli.scanResults.listen((devices) {
scannedDevices.assignAll(devices);
});
// 监听连接状态
_connectionSubscription = jieli.connectionState.listen((state) {
switch (state) {
case ConnectionState.connected:
connectStatus.value = '已连接';
break;
case ConnectionState.disconnected:
connectStatus.value = '已断开';
currentDevice.value = null;
break;
case ConnectionState.connecting:
connectStatus.value = '连接中...';
break;
case ConnectionState.disconnecting:
connectStatus.value = '断开中...';
break;
case ConnectionState.error:
connectStatus.value = '连接错误';
break;
}
});
// 监听唤醒信号
_wakeupSubscription = jieli.wakeupStream.listen((wakeup) {
wakeupReceived.value = true;
Get.snackbar('收到唤醒信号', '耳机发送了唤醒信号');
});
// 监听音频数据
_audioSubscription = jieli.audioDataStream.listen((data) {
// 更新音频数据统计
audioDataSize.value += data.length;
receivedFrames.value++;
audioDataReceiving.value = true;
});
// 监听命令结果
_commandResultSubscription = jieli.commandResultStream.listen((result) {
// 处理音量调整命令结果
if (result.commandType == Jieli.commandTypeVolume) {
volumeCommandResult.value = result.success ? '音量调整成功' : '音量调整失败';
Get.snackbar(
result.success ? '音量调整成功' : '音量调整失败',
result.success ? '设备已更新音量' : '设备未能更新音量',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: result.success ? Colors.green.withOpacity(0.7) : Colors.red.withOpacity(0.7),
colorText: Colors.white,
duration: Duration(seconds: 2),
);
}
});
}
/// 检查蓝牙状态
Future<bool> checkBluetoothStatus() async {
try {
final isEnabled = await jieli.isBluetoothEnabled();
if (!isEnabled) {
return await jieli.requestBluetoothEnable();
}
return true;
} catch (e) {
return false;
}
}
/// 开始扫描设备
Future<void> startScan() async {
if (isScanning.value) return;
final bluetoothReady = await checkBluetoothStatus();
if (!bluetoothReady) {
Get.snackbar('蓝牙未启用', '请启用蓝牙后再试');
return;
}
try {
isScanning.value = true;
// 清空之前的设备列表
scannedDevices.clear();
// 开始扫描,10秒后自动停止
final success = await jieli.startScan(scanTimeout: 10000);
if (!success) {
isScanning.value = false;
Get.snackbar('扫描失败', '启动扫描设备失败,请检查蓝牙权限');
}
// 10秒后自动更新UI状态
Future.delayed(Duration(milliseconds: 10000), () {
isScanning.value = false;
});
} catch (e) {
isScanning.value = false;
Get.snackbar('扫描错误', e.toString());
}
}
/// 停止扫描
Future<void> stopScan() async {
if (!isScanning.value) return;
try {
await jieli.stopScan();
isScanning.value = false;
} catch (e) {
// 停止扫描错误处理
}
}
/// 连接到指定设备
Future<void> connectToDevice(BleDevice device) async {
try {
// 先断开当前连接
if (currentDevice.value != null) {
await disconnectDevice();
}
// 更新UI状态
connectStatus.value = '连接中...';
// 重置音频和唤醒相关的状态
wakeupReceived.value = false;
audioDataSize.value = 0;
receivedFrames.value = 0;
audioDataReceiving.value = false;
// 连接设备
final success = await jieli.connect(device.id);
if (success) {
// 连接成功,更新当前设备
currentDevice.value = device;
deviceInfo.value = '设备名称: ${device.name}\n设备ID: ${device.id}\nRSSI: ${device.rssi} dBm';
} else {
deviceInfo.value = '连接失败';
}
} catch (e) {
deviceInfo.value = '连接错误: ${e.toString()}';
}
}
/// 断开设备连接
Future<void> disconnectDevice() async {
if (currentDevice.value == null) return;
try {
connectStatus.value = '断开中...';
final success = await jieli.disconnect();
if (success) {
deviceInfo.value = '未连接设备';
}
} catch (e) {
// 断开连接错误处理
}
}
/// 发送开始录音命令
Future<void> sendStartRecordingCommand() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
final success = await jieli.sendStartRecordingCommand();
if (success) {
Get.snackbar('命令已发送', '已发送开始录音命令');
} else {
Get.snackbar('命令发送失败', '发送开始录音命令失败');
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
}
}
/// 发送停止录音命令
Future<void> sendStopRecordingCommand() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
final success = await jieli.sendStopRecordingCommand();
if (success) {
Get.snackbar('命令已发送', '已发送停止录音命令');
} else {
Get.snackbar('命令发送失败', '发送停止录音命令失败');
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
}
}
/// 增加音量
Future<void> increaseVolume() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
volumeCommandResult.value = '发送中...';
final success = await jieli.increaseVolume();
if (!success) {
Get.snackbar('命令发送失败', '发送增加音量命令失败');
volumeCommandResult.value = '发送失败';
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
volumeCommandResult.value = '发送出错';
}
}
/// 减小音量
Future<void> decreaseVolume() async {
if (currentDevice.value == null) {
Get.snackbar('操作失败', '未连接设备');
return;
}
try {
volumeCommandResult.value = '发送中...';
final success = await jieli.decreaseVolume();
if (!success) {
Get.snackbar('命令发送失败', '发送减小音量命令失败');
volumeCommandResult.value = '发送失败';
}
} catch (e) {
Get.snackbar('命令错误', e.toString());
volumeCommandResult.value = '发送出错';
}
}
/// 清理资源
void _disposeJieli() {
try {
jieli.dispose();
} catch (e) {
// 释放资源错误处理
}
}
}

568
lib/modules/jieli_test/views/jieli_test_view.dart

@ -0,0 +1,568 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../controllers/jieli_test_controller.dart';
import 'package:jieli/jieli.dart';
class JieliTestView extends GetView<JieliTestController> {
const JieliTestView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: AppBar(
backgroundColor: isDarkMode ? Colors.black : Colors.white,
elevation: 0.5,
title: Text(
'杰理蓝牙测试',
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
),
body: Padding(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildStatusCard(isDarkMode),
SizedBox(height: 16.h),
_buildActionButtons(isDarkMode),
SizedBox(height: 16.h),
_buildDeviceInfo(isDarkMode),
SizedBox(height: 16.h),
_buildDevicesList(isDarkMode),
],
),
),
);
}
Widget _buildStatusCard(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'连接状态: ',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Obx(() => Text(
controller.connectStatus.value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.bold,
color: _getStatusColor(controller.connectStatus.value),
),
)),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(() => _buildStatusChip(
label: '唤醒信号',
isActive: controller.wakeupReceived.value,
activeColor: Colors.orange,
isDarkMode: isDarkMode,
)),
Obx(() => _buildStatusChip(
label: '音频数据',
isActive: controller.audioDataReceiving.value,
activeColor: Colors.green,
isDarkMode: isDarkMode,
)),
],
),
SizedBox(height: 8.h),
Obx(() => controller.audioDataReceiving.value
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'音频数据统计:',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
SizedBox(height: 4.h),
Text(
'已接收 ${(controller.audioDataSize.value / 1024).toStringAsFixed(2)} KB',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
'共 ${controller.receivedFrames.value} 帧',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
)
: SizedBox.shrink()
),
],
),
);
}
Widget _buildStatusChip({
required String label,
required bool isActive,
required Color activeColor,
required bool isDarkMode,
}) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
decoration: BoxDecoration(
color: isActive
? activeColor
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8.w,
height: 8.w,
decoration: BoxDecoration(
color: isActive ? Colors.white : Colors.grey,
shape: BoxShape.circle,
),
),
SizedBox(width: 6.w),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: isActive
? Colors.white
: (isDarkMode ? Colors.white70 : Colors.black54),
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
],
),
);
}
Widget _buildActionButtons(bool isDarkMode) {
return Column(
children: [
Row(
children: [
Expanded(
child: Obx(() => ElevatedButton.icon(
onPressed: controller.isScanning.value
? controller.stopScan
: controller.startScan,
icon: Icon(
controller.isScanning.value ? Icons.stop : Icons.bluetooth_searching,
size: 18.sp,
),
label: Text(
controller.isScanning.value ? '停止扫描' : '扫描设备',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: controller.isScanning.value
? Colors.red[600]
: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
)),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.sendStartRecordingCommand,
icon: Icon(
Icons.mic,
size: 18.sp,
),
label: Text(
'开始录音',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.sendStopRecordingCommand,
icon: Icon(
Icons.stop_circle,
size: 18.sp,
),
label: Text(
'停止录音',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: controller.increaseVolume,
icon: Icon(
Icons.volume_up,
size: 18.sp,
),
label: Text(
'增加音量',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
SizedBox(width: 8.w),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.decreaseVolume,
icon: Icon(
Icons.volume_down,
size: 18.sp,
),
label: Text(
'减小音量',
style: TextStyle(fontSize: 14.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange[600],
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
),
),
],
),
SizedBox(height: 8.h),
Obx(() => controller.volumeCommandResult.value.isNotEmpty
? Container(
width: double.infinity,
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
decoration: BoxDecoration(
color: controller.volumeCommandResult.value.contains('成功')
? Colors.green[100]
: (controller.volumeCommandResult.value == '发送中...'
? Colors.blue[100]
: Colors.red[100]),
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
'音量控制: ${controller.volumeCommandResult.value}',
style: TextStyle(
fontSize: 13.sp,
color: controller.volumeCommandResult.value.contains('成功')
? Colors.green[900]
: (controller.volumeCommandResult.value == '发送中...'
? Colors.blue[900]
: Colors.red[900]),
),
textAlign: TextAlign.center,
),
)
: SizedBox.shrink()
),
],
);
}
Widget _buildDeviceInfo(bool isDarkMode) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备信息:',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: isDarkMode ? Colors.black : Colors.grey[100],
borderRadius: BorderRadius.circular(8.r),
),
child: Obx(() => Text(
controller.deviceInfo.value,
style: TextStyle(
fontSize: 13.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
height: 1.4,
),
)),
),
SizedBox(height: 16.h),
Obx(() => controller.currentDevice.value != null
? ElevatedButton.icon(
onPressed: controller.disconnectDevice,
icon: Icon(
Icons.bluetooth_disabled,
size: 16.sp,
),
label: Text(
'断开连接',
style: TextStyle(fontSize: 13.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[600],
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 10.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
)
: SizedBox.shrink()
),
],
),
);
}
Widget _buildDevicesList(bool isDarkMode) {
return Expanded(
child: Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12.r),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'扫描到的设备',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
Obx(() => Text(
'${controller.scannedDevices.length}个设备',
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
)),
],
),
),
Divider(height: 1, thickness: 1, color: isDarkMode ? Colors.grey[800] : Colors.grey[200]),
Expanded(
child: Obx(() => controller.scannedDevices.isEmpty
? Center(
child: controller.isScanning.value
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16.h),
Text(
'正在扫描...',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
)
: Text(
'未扫描到设备',
style: TextStyle(
fontSize: 14.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
)
: ListView.separated(
padding: EdgeInsets.symmetric(vertical: 8.h),
itemCount: controller.scannedDevices.length,
separatorBuilder: (context, index) => Divider(
height: 1,
thickness: 1,
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
),
itemBuilder: (context, index) {
final device = controller.scannedDevices[index];
return ListTile(
dense: true,
title: Text(
device.name.isEmpty ? '未知设备' : device.name,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'设备ID: ${device.id}',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
Text(
'RSSI: ${device.rssi} dBm',
style: TextStyle(
fontSize: 11.sp,
color: isDarkMode ? Colors.white70 : Colors.black54,
),
),
],
),
trailing: Obx(() => controller.currentDevice.value?.id == device.id
? Chip(
label: Text(
'已连接',
style: TextStyle(
fontSize: 12.sp,
color: Colors.white,
),
),
backgroundColor: Colors.green,
padding: EdgeInsets.symmetric(horizontal: 8.w),
)
: ElevatedButton(
onPressed: () => controller.connectToDevice(device),
child: Text(
'连接',
style: TextStyle(fontSize: 12.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[600],
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.r),
),
),
),
),
contentPadding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
isThreeLine: true,
);
},
),
),
),
],
),
),
);
}
Color _getStatusColor(String status) {
switch (status) {
case '连接中...':
return Colors.orange;
case '已连接':
return Colors.green;
case '连接失败':
return Colors.red;
case '已断开':
return Colors.grey;
default:
return Colors.grey;
}
}
}

20
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:amap_flutter/amap_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
@ -25,6 +26,9 @@ class MeetingRecordController extends GetxController
late AnimationController lottieController;
late AsrService _asrService;
final _amapFlutter = AmapFlutter();
StreamSubscription? _locationStream;
Timer? timer;
RxBool isRecording = false.obs; //是否录音中
@ -33,7 +37,7 @@ class MeetingRecordController extends GetxController
String title = '';
int creationTime = 0;
RxString address = '深圳市高新技术产业园区W2-A座'.obs;
RxString address = '正在定位'.obs;
String _longitude = '';
String _latitude = '';
@ -54,6 +58,7 @@ class MeetingRecordController extends GetxController
final Map<String, dynamic> args = Get.arguments ?? {};
title = args['title'];
creationTime = DateTime.now().millisecondsSinceEpoch;
_amapInit();
}
@override
@ -63,9 +68,22 @@ class MeetingRecordController extends GetxController
_asrService.dispose();
_recognitionSubscription?.cancel();
_originaBytes = BytesBuilder();
_amapFlutter.dispose();
_locationStream?.cancel();
super.onClose();
}
void _amapInit() async {
await _amapFlutter.initLocation();
_locationStream = _amapFlutter.locationStream().listen((data) {
address.value = data['address'];
_longitude = data['longitude'].toString();
_latitude = data['latitude'].toString();
_amapFlutter.stopLocation();
});
_amapFlutter.startLocation();
}
// 点击开始、暂停按钮
void recorderHandle() {
if (isRecording.value) {

19
lib/modules/meeting/views/meeting_details_view.dart

@ -38,7 +38,7 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
onPressed: () => Get.back(),
),
title: Text(
'recordingDetails'.tr,//录音详情
'recordingDetails'.tr, //录音详情
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
@ -89,11 +89,14 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Text(
controller.meetingData.value.address,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
Expanded(
child: Text(
controller.meetingData.value.address,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
overflow: TextOverflow.ellipsis,
),
),
],
@ -253,7 +256,9 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
indicatorColor: isDarkMode ? AppColors.primary : Colors.blue,
labelColor: isDarkMode ? AppColors.primary : Colors.blue,
labelStyle: TextStyle(
color: isDarkMode ? Colors.white : const Color.fromARGB(221, 197, 196, 196),
color: isDarkMode
? Colors.white
: const Color.fromARGB(221, 197, 196, 196),
fontSize: 14.sp,
),
tabs: const [

15
lib/modules/meeting/views/meeting_edit_view.dart

@ -119,12 +119,15 @@ class MeetingEditView extends GetView<MeetingEditController> {
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Obx(
() => Text(
controller.address.value,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
Expanded(
child: Obx(
() => Text(
controller.address.value,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
overflow: TextOverflow.ellipsis,
),
),
),

15
lib/modules/meeting/views/meeting_record_view.dart

@ -135,11 +135,16 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
size: 16.w,
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
),
Text(
controller.address.value,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
Expanded(
child: Obx(
() => Text(
controller.address.value,
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 14.sp,
),
overflow: TextOverflow.ellipsis,
),
),
),
],

4
lib/modules/pairing/controllers/pairing_controller.dart

@ -6,11 +6,12 @@ import '../../../data/services/classic_bluetooth_service.dart';
import '../../../core/utils/logger.dart';
import 'dart:io';
import '../../../data/services/voice_interaction_service.dart';
/// 简化的配对控制器,仅用于显示已连接经典蓝牙设备和处理基本配对流程
class PairingController extends GetxController {
static PairingController get to => Get.find();
// 经典蓝牙服务
final ClassicBluetoothService _bluetoothService =
Get.find<ClassicBluetoothService>();
@ -109,6 +110,7 @@ class PairingController extends GetxController {
/// 初始化流程
Future<void> _initialize() async {
try {
// 初始化语音交互服务
_voiceInteractionService.initialize();

45
lib/modules/profile/controllers/profile_controller.dart

@ -13,7 +13,6 @@ import 'package:path_provider/path_provider.dart';
import '../../../data/services/network/api.dart';
import 'package:dio/dio.dart';
class ProfileController extends GetxController {
static ProfileController get to => Get.find();
@ -118,23 +117,23 @@ class ProfileController extends GetxController {
// _isEditing.value = !_isEditing.value;
// }
// 验证base64图片数据
// 验证base64图片数据
// Future<void> _verifyBase64Image(String base64String) async {
// try {
// // 解码base64数据
// List<int> imageBytes = base64Decode(base64String);
// // 获取应用文档目录
// final appDir = await getApplicationDocumentsDirectory();
// final verifyPath = '${appDir.path}/verify_base64_${DateTime.now().millisecondsSinceEpoch}.jpg';
// // 将解码后的数据写入文件
// final file = File(verifyPath);
// await file.writeAsBytes(imageBytes);
// // 记录验证文件的路径
// Logger().d('Base64验证图片已保存到: $verifyPath');
// // 显示图片预览对话框
// Get.dialog(
// Dialog(
@ -157,7 +156,7 @@ class ProfileController extends GetxController {
// ),
// ),
// );
// Get.snackbar('验证', '已将Base64转换回图片,请查看预览');
// } catch (e) {
// Logger().e('验证Base64图片失败: $e');
@ -171,8 +170,8 @@ class ProfileController extends GetxController {
// 准备数据
Map<String, dynamic> data = {
"image": "", // 默认空字符串
"language": currentLanguage.value,//当前用户语言
"name": userInfo.value.name,//当前用户名
"language": currentLanguage.value, //当前用户语言
"name": userInfo.value.name, //当前用户名
};
// 如果有头像,转换为base64
@ -195,22 +194,21 @@ class ProfileController extends GetxController {
}
}
Logger().d('准备更新用户信息: $data');
// 调用API
final response = await Api.updateUserSetting(data);
Logger().d('--更新用户信息响应: $response');
if(response != null){
if (response != null) {
Logger().d('更新成功: $response');
Get.snackbar('成功', '同步到服务器成功');
userInfo.update((u) {
if (u!= null) {
u.name = response['name']?? '';
u.avatar = response['avatar']?? '';
if (u != null) {
u.name = response['name'] ?? '';
u.avatar = response['avatar'] ?? '';
}
});
});
Logger().d('更新后的用户信息: ${userInfo.value.toJson()}');
}else{
} else {
Logger().e('更新失败: $response');
Get.snackbar('错误', '同步到服务器失败,请重试');
return;
@ -236,10 +234,10 @@ class ProfileController extends GetxController {
}
// 下载并保存远程头像
Future<void> _downloadAndSaveAvatar(String url) async {
Future<void> _downloadAndSaveAvatar(String url) async {
try {
Logger().d('开始下载头像: $url');
final dio = Dio(BaseOptions(
headers: {
'accept': '*/*',
@ -251,9 +249,9 @@ class ProfileController extends GetxController {
url,
options: Options(responseType: ResponseType.bytes),
);
Logger().d('下载状态: ${response.statusCode}');
if (response.statusCode == 200) {
// 保存到应用文档目录
final appDir = await getApplicationDocumentsDirectory();
@ -282,12 +280,12 @@ class ProfileController extends GetxController {
// 更新用户头像
Future<void> updateUserAvatar() async {
final XFile? image = await _picker.pickImage(
source: ImageSource.gallery,// 从相册选择
source: ImageSource.gallery, // 从相册选择
maxWidth: 800,
maxHeight: 800,
imageQuality: 85,
);
if (image != null) {
// 检查文件大小
final file = File(image.path);
@ -356,7 +354,8 @@ class ProfileController extends GetxController {
if (Get.isRegistered<SettingsController>()) {
final settingsController = Get.find<SettingsController>();
settingsController.userInfo.value.name = userInfo.value.name;
settingsController.userInfo.value.avatar = '${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}';
settingsController.userInfo.value.avatar =
'${userInfo.value.avatar}?time=${DateTime.now().millisecondsSinceEpoch}';
//settingsController.loadUserInfo();
settingsController.userInfo.refresh();
}

22
lib/modules/settings/views/settings_view.dart

@ -475,6 +475,28 @@ class SettingsView extends GetView<SettingsController> {
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 杰理测试
_buildSimpleNavigationSetting(
title: '杰理蓝牙测试',
subtitle: '测试杰理蓝牙设备扫描、连接和功能',
icon: Icons.bluetooth,
iconBgColor: isDarkMode
? Colors.blue[900]!.withOpacity(0.3)
: Colors.blue[100]!,
iconColor:
isDarkMode ? Colors.blue[300]! : Colors.blue[600]!,
titleColor: isDarkMode ? Colors.white : null,
subtitleColor: isDarkMode ? Colors.white70 : null,
onTap: () {
Get.toNamed(Routes.jieliTest);
},
isDarkMode: isDarkMode,
),
Divider(
height: 1,
color: isDarkMode

7
lib/modules/splash/controllers/splash_controller.dart

@ -1,4 +1,5 @@
import 'dart:async';
import 'package:amap_flutter/amap_flutter.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
@ -29,12 +30,16 @@ class SplashController extends GetxController {
WidgetsBinding.instance.addPostFrameCallback((_) {
_startLoadingAnimation();
_loadAppResources();
AmapFlutter().initSdk(
iosKey: "a4a1394fe817c2f86a424b897b4a9af4",
androidKey: "e5fdc9605eabdeb5626f18f5721f343d",
);
});
}
// 进度条动画(持续2秒)
void _startLoadingAnimation() {
const totalDuration = 2000; // 动画总时长2秒
const totalDuration = 500; // 动画总时长2秒
const steps = totalDuration ~/ 50; // 40次更新(50ms间隔)
final stepValue = 1.0 / steps;

7
lib/routes/app_pages.dart

@ -33,6 +33,8 @@ import '../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart';
import '../modules/settings/views/settings_view.dart';
import '../modules/settings/bindings/settings_binding.dart';
import '../modules/jieli_test/views/jieli_test_view.dart';
import '../modules/jieli_test/bindings/jieli_test_binding.dart';
abstract class AppPages {
static final pages = [
@ -126,5 +128,10 @@ abstract class AppPages {
page: () => const FeedbackView(),
binding: FeedbackBinding(),
)
GetPage(
name: Routes.jieliTest,
page: () => const JieliTestView(),
binding: JieliTestBinding(),
),
];
}

1
lib/routes/app_routes.dart

@ -25,4 +25,5 @@ abstract class Routes {
static const FLUTTER_ASR_TEST = '/flutter_asr_test';
static const editProfile = '/edit_profile';
static const feedback = '/feedback';
static const jieliTest = '/jieli_test';
}

0
local_plugins/azure_speech/LICENSE

136
local_plugins/azure_speech/README.md

@ -0,0 +1,136 @@
# Azure Speech 插件
本插件为Flutter提供了Azure语音服务的集成,包括:
- 语音合成(TTS)
- 语音识别(ASR)
## 功能
### 语音合成(TTS)
- 支持多种语音(如中文、英文等)
- 语音参数调整(语速、音调、音量)
- 音频输出设备选择(扬声器、听筒、自动)
- SSML支持
### 语音识别(ASR)
- 一次性语音识别
- 连续语音识别
- 自动语言检测
- 音频处理优化(回音消除、噪声抑制等)
## 平台支持
- Android
- iOS
## 如何使用
### 初始化
```dart
import 'package:azure_speech/azure_speech.dart';
// 初始化TTS
await AzureSpeech.initializeTts(
'your_subscription_key',
'your_service_region',
language: 'zh-CN',
);
// 初始化ASR
await AzureSpeech.initializeAsr(
'your_subscription_key',
'your_service_region',
['zh-CN', 'en-US'],
);
```
### 语音合成
```dart
// 设置语音
await AzureSpeech.setTtsVoice('zh-CN-XiaoxiaoNeural');
// 设置语音参数
await AzureSpeech.setTtsSpeechParams(
rate: 0, // 语速 -100~100
pitch: 0, // 音调 -100~100
volume: 100, // 音量 0~100
);
// 设置音频输出设备
await AzureSpeech.setTtsAudioOutputType('AUTO'); // 'SPEAKER', 'EARPIECE', 'AUTO'
// 播放文本
await AzureSpeech.speakText('你好,世界!');
// 停止播放
await AzureSpeech.stopSpeaking();
// 检查是否正在播放
bool isSpeaking = await AzureSpeech.isSpeaking();
```
### 语音识别
```dart
// 一次性识别
final result = await AzureSpeech.recognizeOnce();
if (result['success']) {
print('识别文本: ${result['text']}');
print('识别语言: ${result['language']}');
} else {
print('识别失败: ${result['error']}');
}
// 连续识别
// 监听识别结果
AzureSpeech.asrResultStream.listen((event) {
switch (event['eventType']) {
case 'recognizing':
// 实时识别中的结果
print('识别中: ${event['text']}');
break;
case 'finalResult':
// 最终识别结果
print('最终结果: ${event['text']}');
break;
case 'error':
// 错误
print('错误: ${event['error']}');
break;
}
});
// 开始连续识别
await AzureSpeech.startContinuousRecognition();
// 停止连续识别
await AzureSpeech.stopContinuousRecognition();
// 检查连续识别是否活跃
bool isActive = await AzureSpeech.isContinuousRecognitionActive();
```
### 释放资源
```dart
// 释放所有资源
await AzureSpeech.dispose();
```
## 依赖项
本插件依赖于:
- Microsoft Cognitive Services Speech SDK
- Flutter
## 注意事项
- 使用前需要在Azure门户中创建语音服务资源,并获取订阅密钥和区域
- Android需要相关权限:RECORD_AUDIO, INTERNET等
- iOS需要在Info.plist中添加麦克风使用权限描述

56
local_plugins/azure_speech/android/build.gradle.kts

@ -0,0 +1,56 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
namespace = "com.yunqiinnovation.azure_speech"
compileSdk = 33
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
sourceSets {
getByName("main") {
manifest.srcFile("src/main/AndroidManifest.xml")
java.srcDirs("src/main/kotlin")
}
}
// 添加lint选项
lintOptions {
isCheckReleaseBuilds = false
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
// 显式设置Kotlin JVM目标版本
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
jvmTarget = "11"
}
}
dependencies {
// 添加Microsoft语音SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0")
}

1
local_plugins/azure_speech/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "azure_speech"

6
local_plugins/azure_speech/android/src/main/AndroidManifest.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.azure_speech">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
</manifest>

593
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt

@ -0,0 +1,593 @@
package com.yunqiinnovation.azure_speech
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioRecord
import android.media.MediaRecorder
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.os.Process
import com.yunqiinnovation.azure_speech.utils.FileLogger
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicBoolean
class AzureAsrHelper(private val context: Context) {
private var recognizer: SpeechRecognizer? = null
private var speechConfig: SpeechConfig? = null
private val TAG = "AzureAsrHelper"
private var isContinuousRecognitionActive = false
private var currentLanguage = "zh-CN"
private var subscriptionKey = ""
private var region = ""
private var isAutoDetectLanguage = false
private var supportedLanguages = arrayOf("zh-CN")
// 是否使用回音消除 - 内部控制常量
private val useEchoCancellation = false
// 自定义音频处理相关
private var customAudioProcessor: CustomAudioProcessor? = null
private var pushStream: PushAudioInputStream? = null
private var audioConfig: AudioConfig? = null
// 初始化SDK并创建recognizer
fun initialize(subscriptionKey: String, region: String,
supportedLanguages: Array<String> = arrayOf("zh-CN")): Boolean {
try {
FileLogger.d(TAG, "初始化 Azure 语音服务")
// 检查配置是否为空
if (subscriptionKey.isEmpty() || region.isEmpty()) {
FileLogger.e(TAG, "Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
this.subscriptionKey = subscriptionKey
this.region = region
// 设置语言
if (supportedLanguages.isNotEmpty()) {
this.supportedLanguages = supportedLanguages
}
// 根据支持的语言数量决定是否启用自动语言检测
this.isAutoDetectLanguage = supportedLanguages.size >= 2
// 如果只有一种语言,设置为当前语言
if (!isAutoDetectLanguage && supportedLanguages.isNotEmpty()) {
this.currentLanguage = supportedLanguages[0]
}
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region)
// 设置语言配置
if (isAutoDetectLanguage) {
// 设置自动语言检测
speechConfig?.setProperty(PropertyId.SpeechServiceConnection_LanguageIdMode, "Continuous")
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
}
// 创建识别器
try {
if (useEchoCancellation) {
// 如果使用回音消除,创建自定义音频输入流
setupCustomAudioProcessing()
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} else {
// 使用默认麦克风输入
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
}
FileLogger.d(TAG, "Azure 语音服务初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "创建识别器失败: ${e.message}")
stopCustomAudioProcessing()
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}")
return false
}
}
// 重置 recognizer
private fun resetRecognizer(): Boolean {
try {
// 释放之前的 recognizer
recognizer?.close()
recognizer = null
// 停止当前的音频处理
stopCustomAudioProcessing()
// 使用现有配置重新创建 recognizer
if (speechConfig != null) {
if (useEchoCancellation) {
// 如果使用回音消除,创建自定义音频输入流
setupCustomAudioProcessing()
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig, audioConfig)
} else {
recognizer = SpeechRecognizer(speechConfig, audioConfig)
}
} else {
// 使用默认麦克风输入
if (isAutoDetectLanguage) {
val autoDetectConfig = AutoDetectSourceLanguageConfig.fromLanguages(supportedLanguages.toList())
recognizer = SpeechRecognizer(speechConfig, autoDetectConfig)
} else {
recognizer = SpeechRecognizer(speechConfig)
}
}
return true
} else {
FileLogger.e(TAG, "语音配置未初始化")
return false
}
} catch (e: Exception) {
FileLogger.e(TAG, "重置识别器失败: ${e.message}")
return false
}
}
// 开始一次性语音识别
fun recognizeOnce(callback: RecognizeCallback) {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return
}
// 重置 recognizer
if (!resetRecognizer()) {
callback.onError("重置识别器失败")
return
}
try {
// 启动音频处理
startCustomAudioProcessing()
// 执行识别
val result = recognizer?.recognizeOnceAsync()?.get()
// 停止音频处理
stopCustomAudioProcessing()
if (result != null && result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(result)?.language
callback.onResult(result.text, detectedLanguage ?: "")
} else {
callback.onError("未能识别语音")
}
} catch (e: Exception) {
// 停止音频处理
stopCustomAudioProcessing()
callback.onError("识别异常: ${e.message}")
}
}
// 开始连续语音识别
fun startContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return false
}
if (isContinuousRecognitionActive) {
FileLogger.w(TAG, "已在进行连续识别,忽略请求")
return true
}
// 重置 recognizer
if (!resetRecognizer()) {
callback.onError("重置识别器失败")
return false
}
try {
// 启动音频处理
startCustomAudioProcessing()
// 识别中事件
recognizer?.recognizing?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language
FileLogger.d(TAG, "识别中: ${event.result.text}, 语言: $detectedLanguage")
callback.onRecognizing(event.result.text, detectedLanguage ?: "")
}
)
// 识别完成事件
recognizer?.recognized?.addEventListener(
EventHandler<SpeechRecognitionEventArgs> { _, event ->
if (event.result.reason == ResultReason.RecognizedSpeech) {
val detectedLanguage = AutoDetectSourceLanguageResult.fromResult(event.result)?.language
FileLogger.d(TAG, "识别完成: ${event.result.text}, 语言: $detectedLanguage")
callback.onResult(event.result.text, detectedLanguage ?: "")
}
}
)
// 会话开始事件
recognizer?.sessionStarted?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
FileLogger.d(TAG, "识别会话已开始")
callback.onSessionStarted()
callback.onSuccess("开始识别") // 兼容旧接口
}
)
// 会话结束事件
recognizer?.sessionStopped?.addEventListener(
EventHandler<SessionEventArgs> { _, _ ->
FileLogger.d(TAG, "识别会话已结束")
isContinuousRecognitionActive = false
stopCustomAudioProcessing()
callback.onSessionStopped()
}
)
// 取消事件
recognizer?.canceled?.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = try {
event.errorDetails ?: "未知错误"
} catch (e: Exception) {
"未知错误"
}
val reason = event.reason.toString()
FileLogger.e(TAG, "识别取消: $errorDetails")
isContinuousRecognitionActive = false
stopCustomAudioProcessing()
callback.onCanceled(reason, errorDetails)
callback.onError("识别取消: $errorDetails") // 兼容旧接口
}
)
// 开始连续识别
recognizer?.startContinuousRecognitionAsync()
isContinuousRecognitionActive = true
FileLogger.d(TAG, "连续识别已启动")
return true
} catch (e: Exception) {
// 停止音频处理
stopCustomAudioProcessing()
FileLogger.e(TAG, "开始连续识别失败: ${e.message}")
e.printStackTrace()
callback.onError("开始连续识别失败: ${e.message}")
return false
}
}
// 停止连续语音识别
fun stopContinuousRecognition(callback: ContinuousRecognizeCallback): Boolean {
if (speechConfig == null) {
callback.onError("语音服务未初始化")
return false
}
if (!isContinuousRecognitionActive) {
FileLogger.d(TAG, "未进行连续识别,忽略停止请求")
return true
}
try {
FileLogger.d(TAG, "停止连续语音识别")
if (recognizer == null) {
if (isContinuousRecognitionActive) {
FileLogger.w(TAG, "识别器为空,但状态显示活跃")
}
isContinuousRecognitionActive = false
return true
}
// 停止连续识别
val future = recognizer?.stopContinuousRecognitionAsync()
future?.get()
// 停止音频处理
stopCustomAudioProcessing()
isContinuousRecognitionActive = false
FileLogger.d(TAG, "连续识别已停止")
callback.onSuccess("连续识别已停止")
return true
} catch (e: Exception) {
// 强制重置状态
isContinuousRecognitionActive = false
FileLogger.e(TAG, "停止连续识别失败: ${e.message}")
e.printStackTrace()
callback.onError("停止连续识别失败: ${e.message}")
// 停止音频处理
stopCustomAudioProcessing()
// 尝试强制关闭识别器
try {
recognizer?.close()
recognizer = null
} catch (ex: Exception) {
FileLogger.e(TAG, "关闭识别器失败: ${ex.message}")
}
return false
}
}
// 释放资源
fun dispose() {
try {
// 如果正在进行连续识别,先停止
if (isContinuousRecognitionActive) {
recognizer?.stopContinuousRecognitionAsync()?.get()
isContinuousRecognitionActive = false
}
// 停止音频处理
stopCustomAudioProcessing()
// 释放recognizer
recognizer?.close()
recognizer = null
// 释放speechConfig
speechConfig?.close()
speechConfig = null
FileLogger.d(TAG, "资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源失败: ${e.message}")
// 确保状态被重置
isContinuousRecognitionActive = false
customAudioProcessor = null
pushStream = null
audioConfig = null
recognizer = null
speechConfig = null
}
}
// 检查连续识别是否处于活跃状态
fun isContinuousRecognitionActive(): Boolean {
return isContinuousRecognitionActive
}
// 设置自定义音频处理
private fun setupCustomAudioProcessing() {
try {
// 创建音频推送流
pushStream = PushAudioInputStream.create()
// 创建音频配置
audioConfig = AudioConfig.fromStreamInput(pushStream)
// 创建自定义音频处理器
customAudioProcessor = CustomAudioProcessor(pushStream!!)
} catch (e: Exception) {
FileLogger.e(TAG, "设置自定义音频处理失败: ${e.message}")
e.printStackTrace()
}
}
// 启动自定义音频处理
private fun startCustomAudioProcessing() {
if (useEchoCancellation && customAudioProcessor != null) {
try {
customAudioProcessor?.startProcessing()
} catch (e: Exception) {
FileLogger.e(TAG, "启动音频处理器失败")
e.printStackTrace()
}
}
}
// 停止自定义音频处理
private fun stopCustomAudioProcessing() {
if (customAudioProcessor != null) {
try {
customAudioProcessor?.stopProcessing()
customAudioProcessor = null
} catch (e: Exception) {
FileLogger.e(TAG, "停止音频处理器失败: ${e.message}")
e.printStackTrace()
}
}
}
// 修改认证取消事件处理代码
private fun setupCancelledEventHandler(callback: ContinuousRecognizeCallback) {
recognizer?.canceled?.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = try {
event.errorDetails ?: "未知错误"
} catch (e: Exception) {
"未知错误"
}
val reason = event.reason.toString()
FileLogger.e(TAG, "识别取消: $errorDetails")
isContinuousRecognitionActive = false
stopCustomAudioProcessing()
callback.onCanceled(reason, errorDetails)
callback.onError("识别取消: $errorDetails") // 兼容旧接口
}
)
}
// 自定义音频处理器
private inner class CustomAudioProcessor(private val pushStream: PushAudioInputStream) {
private val isProcessing = AtomicBoolean(false)
private var audioRecord: AudioRecord? = null
private var echoCanceler: AcousticEchoCanceler? = null
private var noiseSuppressor: NoiseSuppressor? = null
private var automaticGainControl: AutomaticGainControl? = null
// 音频配置
private val sampleRate = 16000 // 16kHz,适合语音识别
private val channelConfig = AudioFormat.CHANNEL_IN_MONO
private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
// 计算最小 buffer 大小
private val bufferSize = AudioRecord.getMinBufferSize(
sampleRate, channelConfig, audioFormat
)
// 启动音频处理
fun startProcessing() {
if (isProcessing.get()) return
// 创建录音对象
try {
audioRecord = AudioRecord(
MediaRecorder.AudioSource.VOICE_RECOGNITION,
sampleRate,
channelConfig,
audioFormat,
bufferSize * 2 // 使用更大的缓冲区以确保不会丢失数据
)
// 创建音频处理效果
if (AcousticEchoCanceler.isAvailable()) {
echoCanceler = AcousticEchoCanceler.create(audioRecord!!.audioSessionId)
echoCanceler?.enabled = true
}
if (NoiseSuppressor.isAvailable()) {
noiseSuppressor = NoiseSuppressor.create(audioRecord!!.audioSessionId)
noiseSuppressor?.enabled = true
}
if (AutomaticGainControl.isAvailable()) {
automaticGainControl = AutomaticGainControl.create(audioRecord!!.audioSessionId)
automaticGainControl?.enabled = true
}
// 开始录音
audioRecord?.startRecording()
// 处理线程
Thread {
android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO)
processAudio()
}.start()
isProcessing.set(true)
FileLogger.d(TAG, "音频处理已启动")
} catch (e: Exception) {
FileLogger.e(TAG, "创建音频处理器失败: ${e.message}")
releaseAudioResources()
throw e
}
}
// 停止音频处理
fun stopProcessing() {
if (!isProcessing.get()) return
isProcessing.set(false)
releaseAudioResources()
FileLogger.d(TAG, "音频处理已停止")
}
// 释放音频资源
private fun releaseAudioResources() {
try {
audioRecord?.stop()
echoCanceler?.release()
echoCanceler = null
noiseSuppressor?.release()
noiseSuppressor = null
automaticGainControl?.release()
automaticGainControl = null
audioRecord?.release()
audioRecord = null
} catch (e: Exception) {
FileLogger.e(TAG, "释放音频资源失败: ${e.message}")
}
}
// 音频处理线程
private fun processAudio() {
// 设置线程优先级
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO)
} catch (e: Exception) {
FileLogger.e(TAG, "设置线程优先级失败")
}
val buffer = ByteArray(bufferSize)
while (isProcessing.get()) {
try {
val readSize = audioRecord?.read(buffer, 0, buffer.size) ?: -1
if (readSize > 0) {
// 修复: 只传入buffer,不传readSize
// 创建新的byte数组,只包含读取到的数据
val audioData = buffer.copyOfRange(0, readSize)
pushStream.write(audioData)
}
// 适当休眠,避免占用过多 CPU
Thread.sleep(5)
} catch (e: Exception) {
if (isProcessing.get()) {
FileLogger.e(TAG, "处理音频数据异常: ${e.message}")
}
break
}
}
}
}
// 一次性识别回调接口
interface RecognizeCallback {
fun onResult(text: String, detectedLanguage: String)
fun onError(error: String)
}
// 连续识别回调接口
interface ContinuousRecognizeCallback {
fun onResult(text: String, detectedLanguage: String)
fun onRecognizing(recognizing: String, detectedLanguage: String)
fun onSessionStarted()
fun onSessionStopped()
fun onCanceled(reason: String, errorDetails: String)
fun onError(error: String)
// 兼容旧版本的接口
fun onSuccess(message: String) {}
}
}

297
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt

@ -0,0 +1,297 @@
package com.yunqiinnovation.azure_speech
import android.content.Context
import android.app.Activity
import android.os.Handler
import android.os.Looper
import androidx.annotation.NonNull
import com.yunqiinnovation.azure_speech.utils.FileLogger
import io.flutter.embedding.engine.plugins.FlutterPlugin
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
import io.flutter.plugin.common.EventChannel
/** AzureSpeechPlugin */
class AzureSpeechPlugin: FlutterPlugin {
private val TAG = "AzureSpeechPlugin"
private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper())
// ASR相关
private lateinit var asrChannel : MethodChannel
private lateinit var asrEventChannel: EventChannel
private var asrEventSink: EventChannel.EventSink? = null
private lateinit var azureAsrHelper: AzureAsrHelper
// TTS相关
private lateinit var ttsChannel : MethodChannel
private lateinit var azureTtsHelper: AzureTtsHelper
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
FileLogger.d(TAG, "发送ASR事件: $event")
if (asrEventSink == null) {
FileLogger.w(TAG, "无法发送ASR事件:事件通道未准备好")
return
}
mainHandler.post {
try {
asrEventSink?.success(event)
FileLogger.d(TAG, "ASR事件发送成功")
} catch (e: Exception) {
FileLogger.e(TAG, "发送ASR事件失败: ${e.message}")
}
}
}
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
// 初始化ASR通道
asrChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr")
asrChannel.setMethodCallHandler(AsrMethodHandler())
// 初始化TTS通道
ttsChannel = MethodChannel(flutterPluginBinding.binaryMessenger, "azure_speech/tts")
ttsChannel.setMethodCallHandler(TtsMethodHandler())
// 初始化ASR事件通道
asrEventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "azure_speech/asr_events")
asrEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
asrEventSink = events
}
override fun onCancel(arguments: Any?) {
asrEventSink = null
}
})
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
}
// ASR方法处理器
inner class AsrMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val supportedLanguages = call.argument<List<String>>("supportedLanguages") ?: listOf("zh-CN")
try {
val success = azureAsrHelper.initialize(subscriptionKey, region, supportedLanguages.toTypedArray())
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", e.message, null)
}
}
"recognizeOnce" -> {
azureAsrHelper.recognizeOnce(object : AzureAsrHelper.RecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
mainHandler.post {
result.success(mapOf(
"text" to text,
"detectedLanguage" to detectedLanguage
))
}
}
override fun onError(error: String) {
mainHandler.post {
result.error("RECOGNITION_ERROR", error, null)
}
}
})
}
"startContinuousRecognition" -> {
// 确保事件通道已准备好
if (asrEventSink == null) {
result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null)
return
}
val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
"type" to "result",
"text" to text,
"detectedLanguage" to detectedLanguage
))
}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
sendAsrEvent(mapOf(
"type" to "recognizing",
"text" to recognizing,
"detectedLanguage" to detectedLanguage
))
}
override fun onSessionStarted() {
sendAsrEvent(mapOf("type" to "sessionStarted"))
}
override fun onSessionStopped() {
sendAsrEvent(mapOf("type" to "sessionStopped"))
}
override fun onCanceled(reason: String, errorDetails: String) {
sendAsrEvent(mapOf(
"type" to "canceled",
"reason" to reason,
"errorDetails" to errorDetails
))
}
override fun onError(error: String) {
sendAsrEvent(mapOf("type" to "error", "message" to error))
}
override fun onSuccess(message: String) {
sendAsrEvent(mapOf("type" to "success", "message" to message))
}
})
result.success(success)
}
"stopContinuousRecognition" -> {
try {
if (!azureAsrHelper.isContinuousRecognitionActive()) {
result.success(true)
return
}
val success = azureAsrHelper.stopContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onResult(text: String, detectedLanguage: String) {}
override fun onRecognizing(recognizing: String, detectedLanguage: String) {}
override fun onSessionStarted() {}
override fun onSessionStopped() {}
override fun onCanceled(reason: String, errorDetails: String) {}
override fun onError(error: String) {
mainHandler.post {
result.error("STOP_ERROR", error, null)
}
}
override fun onSuccess(message: String) {}
})
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", e.message, null)
}
}
"isContinuousRecognitionActive" -> {
result.success(azureAsrHelper.isContinuousRecognitionActive())
}
"dispose" -> {
azureAsrHelper.dispose()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
}
// TTS方法处理器
inner class TtsMethodHandler : MethodCallHandler {
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
when (call.method) {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val language = call.argument<String>("language") ?: "zh-CN"
val success = azureTtsHelper.initialize(subscriptionKey, region, language)
result.success(success)
}
"setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
result.success(azureTtsHelper.setVoice(voiceName))
}
"setSpeechParams" -> {
val rate = call.argument<Int>("rate") ?: 0
val pitch = call.argument<Int>("pitch") ?: 0
val volume = call.argument<Int>("volume") ?: 100
result.success(azureTtsHelper.setSpeechParams(rate, pitch, volume))
}
"setAudioOutputType" -> {
val outputTypeStr = call.argument<String>("outputType") ?: "speaker"
val outputType = when (outputTypeStr.lowercase()) {
"speaker" -> AzureTtsHelper.AudioOutputType.SPEAKER
"earpiece" -> AzureTtsHelper.AudioOutputType.EARPIECE
"auto" -> AzureTtsHelper.AudioOutputType.AUTO
else -> AzureTtsHelper.AudioOutputType.SPEAKER
}
result.success(azureTtsHelper.setAudioOutputType(outputType))
}
"speakText" -> {
val text = call.argument<String>("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null)
azureTtsHelper.speakText(text, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
mainHandler.post {
result.success(true)
}
}
override fun onError(error: String) {
mainHandler.post {
result.error("SPEAK_ERROR", error, null)
}
}
})
}
"speakSsml" -> {
val ssml = call.argument<String>("ssml") ?: return result.error("INVALID_ARGUMENTS", "SSML不能为空", null)
azureTtsHelper.speakSsml(ssml, object : AzureTtsHelper.TTSCallback {
override fun onSuccess(message: String) {
mainHandler.post {
result.success(true)
}
}
override fun onError(error: String) {
mainHandler.post {
result.error("SPEAK_ERROR", error, null)
}
}
})
}
"stopSpeaking" -> {
result.success(azureTtsHelper.stopSpeaking())
}
"isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking())
}
"dispose" -> {
azureTtsHelper.dispose()
result.success(true)
}
else -> {
result.notImplemented()
}
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
asrChannel.setMethodCallHandler(null)
ttsChannel.setMethodCallHandler(null)
asrEventChannel.setStreamHandler(null)
try {
azureTtsHelper.dispose()
azureAsrHelper.dispose()
} catch (e: Exception) {
FileLogger.e(TAG, "Dispose resources error: ${e.message}")
}
}
}

12
android/app/src/main/kotlin/com/example/deep_voice/AzureTtsHelper.kt → local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -1,11 +1,11 @@
package com.yunqiinnovation.deepsound
package com.yunqiinnovation.azure_speech
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioDeviceInfo
import android.media.AudioManager
import android.os.Build
import com.yunqiinnovation.deepsound.core.utils.FileLogger
import com.yunqiinnovation.azure_speech.utils.FileLogger
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import java.util.concurrent.Future
@ -50,16 +50,16 @@ class AzureTtsHelper(private val context: Context) {
* 初始化 TTS 引擎
*
* @param subscriptionKey Azure 语音服务订阅密钥
* @param serviceRegion Azure 语音服务区域
* @param region Azure 语音服务区域
* @param language 可选,默认语言,默认为 "zh-CN"
*/
fun initialize(subscriptionKey: String, serviceRegion: String, language: String = "zh-CN"): Boolean {
fun initialize(subscriptionKey: String, region: String, language: String = "zh-CN"): Boolean {
try {
audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region)
// 设置语音合成输出格式为高质量音频
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
@ -96,7 +96,7 @@ class AzureTtsHelper(private val context: Context) {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "TTS 引擎初始化失败: ${e.message}, ${serviceRegion}")
FileLogger.e(TAG, "TTS 引擎初始化失败: ${e.message}, ${region}")
e.printStackTrace()
return false
}

45
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/utils/FileLogger.kt

@ -0,0 +1,45 @@
package com.yunqiinnovation.azure_speech.utils
import android.util.Log
/**
* 简单的文件日志工具类
*/
object FileLogger {
private const val TAG_PREFIX = "AzureSpeech_"
/**
* 记录调试信息
*/
fun d(tag: String, message: String) {
Log.d("$TAG_PREFIX$tag", message)
}
/**
* 记录信息
*/
fun i(tag: String, message: String) {
Log.i("$TAG_PREFIX$tag", message)
}
/**
* 记录警告信息
*/
fun w(tag: String, message: String) {
Log.w("$TAG_PREFIX$tag", message)
}
/**
* 记录错误信息
*/
fun e(tag: String, message: String) {
Log.e("$TAG_PREFIX$tag", message)
}
/**
* 记录异常
*/
fun e(tag: String, message: String, throwable: Throwable) {
Log.e("$TAG_PREFIX$tag", message, throwable)
}
}

0
local_plugins/azure_speech/ios/Classes/.gitkeep

461
local_plugins/azure_speech/ios/Classes/AzureAsrHelper.swift

@ -0,0 +1,461 @@
import Foundation
import AVFoundation
import MicrosoftCognitiveServicesSpeech
/// Azure 语音识别辅助类
class AzureAsrHelper: NSObject {
private var recognizer: SPXSpeechRecognizer?
private var speechConfig: SPXSpeechConfig?
private var audioConfig: SPXAudioConfig?
private var initialized = false
private var isContinuousRecognitionActive = false
private var currentLanguage = "zh-CN"
private var subscriptionKey = ""
private var serviceRegion = ""
private var isAutoDetectLanguage = false
private var supportedLanguages = ["zh-CN", "en-US"]
// 音频会话管理
private let audioSession = AVAudioSession.sharedInstance()
// 事件回调
private var eventHandler: (([String: Any]) -> Void)?
/// 设置事件处理器
///
/// - Parameter handler: 事件处理回调
func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
/// 初始化语音识别服务
///
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 语音服务区域
/// - supportedLanguages: 支持的语言列表,默认为 ["zh-CN", "en-US"]
/// - Returns: 是否初始化成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, supportedLanguages: [String] = ["zh-CN", "en-US"]) -> Bool {
print("[AzureAsrHelper] 初始化 Azure 语音服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
// 保存配置
self.subscriptionKey = speechSubscriptionKey
self.serviceRegion = serviceRegion
// 设置语言
if supportedLanguages.isEmpty {
print("[AzureAsrHelper] 警告: 传入的支持语言列表为空,将使用默认语言")
} else {
self.supportedLanguages = supportedLanguages
}
// 根据支持的语言数量决定是否启用自动语言检测
self.isAutoDetectLanguage = supportedLanguages.count >= 2
// 如果只有一种语言,设置为当前语言
if !isAutoDetectLanguage && !supportedLanguages.isEmpty {
self.currentLanguage = supportedLanguages[0]
}
// 创建语音配置
do {
speechConfig = try SPXSpeechConfig(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置语言配置
if isAutoDetectLanguage {
// 设置自动语言检测
try speechConfig?.setPropertyTo("Continuous", byId: SPXPropertyId.SpeechServiceConnection_LanguageIdMode)
} else {
// 设置指定的识别语言
speechConfig?.speechRecognitionLanguage = currentLanguage
}
// 创建音频配置 - 使用默认麦克风
audioConfig = SPXAudioConfig.default()
// 创建识别器
if isAutoDetectLanguage {
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguages: supportedLanguages)
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, audioConfiguration: audioConfig!)
} else {
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
}
// 配置音频会话
try configureAudioSession()
initialized = true
print("[AzureAsrHelper] Azure 语音服务初始化成功")
return true
} catch {
print("[AzureAsrHelper] 错误: 创建识别器失败: \(error.localizedDescription)")
return false
}
}
/// 配置音频会话
private func configureAudioSession() throws {
print("[AzureAsrHelper] 开始配置音频会话...")
do {
// 设置音频会话类别和模式
try audioSession.setCategory(.record, mode: .measurement, options: [.duckOthers, .allowBluetooth])
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("[AzureAsrHelper] 警告: 通过AudioSessionManager配置音频会话失败")
throw error
}
}
/// 一次性语音识别
///
/// - Parameter completion: 完成回调,返回是否成功、识别文本、识别语言和可能的错误信息
func recognizeOnce(completion: @escaping (Bool, String?, String?, String?) -> Void) {
if !initialized {
completion(false, nil, nil, "语音服务未初始化")
return
}
// 重置 recognizer
if !resetRecognizer() {
completion(false, nil, nil, "重置识别器失败")
return
}
do {
// 激活音频会话
try audioSession.setActive(true)
// 添加识别事件处理
recognizer?.addRecognizedEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == .recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
print("[AzureAsrHelper] 识别结果: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
completion(true, event.result.text, detectedLanguage, nil)
}
}
recognizer?.addRecognizingEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == .recognizingSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
// print("[AzureAsrHelper] 识别中: \(event.result.text ?? ""), 语言: \(detectedLanguage)")
}
}
// 添加会话事件处理
recognizer?.addSessionStartedEventHandler { _, _ in
print("[AzureAsrHelper] 识别会话已开始")
}
recognizer?.addSessionStoppedEventHandler { _, _ in
print("[AzureAsrHelper] 识别会话已结束")
}
// 添加取消事件处理
recognizer?.addCanceledEventHandler { _, event in
if let cancellationDetails = try? SPXCancellationDetails(fromCanceledRecognitionResult: event.result) {
let errorDetails = cancellationDetails.errorDetails ?? "未知错误"
print("[AzureAsrHelper] 识别取消: \(errorDetails)")
completion(false, nil, nil, "识别取消: \(errorDetails)")
}
}
// 执行识别
let result = try recognizer?.recognizeOnceAsync().get()
if result?.reason != .recognizedSpeech {
completion(false, nil, nil, "未能识别语音")
}
} catch {
completion(false, nil, nil, "识别异常: \(error.localizedDescription)")
}
}
/// 重置识别器
///
/// - Returns: 是否重置成功
private func resetRecognizer() -> Bool {
if !initialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
return false
}
// 检查配置是否为空
if subscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
return false
}
do {
// 释放之前的 recognizer
recognizer = nil
// 创建音频配置 - 使用默认麦克风
audioConfig = SPXAudioConfig.default()
// 重新创建识别器
if isAutoDetectLanguage {
let autoDetectConfig = try SPXAutoDetectSourceLanguageConfiguration(sourceLanguages: supportedLanguages)
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, autoDetectSourceLanguageConfiguration: autoDetectConfig, audioConfiguration: audioConfig!)
} else {
recognizer = try SPXSpeechRecognizer(speechConfiguration: speechConfig!, audioConfiguration: audioConfig!)
}
return true
} catch {
print("[AzureAsrHelper] 错误: 重置识别器失败: \(error.localizedDescription)")
return false
}
}
/// 获取检测到的语言
///
/// - Parameter result: 识别结果
/// - Returns: 检测到的语言代码
private func getDetectedLanguage(from result: SPXSpeechRecognitionResult) -> String {
if isAutoDetectLanguage {
do {
if let autoDetectResult = try SPXAutoDetectSourceLanguageResult(fromRecognitionResult: result) {
return autoDetectResult.language
}
return ""
} catch {
print("[AzureAsrHelper] 错误: 获取检测到的语言失败: \(error.localizedDescription)")
return ""
}
} else {
return currentLanguage
}
}
/// 开始连续语音识别
///
/// - Returns: 是否成功启动连续识别
func startContinuousRecognition() -> Bool {
if !initialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
return false
}
// 检查配置是否为空
if subscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
return false
}
// 如果已经在进行连续识别,直接返回
if isContinuousRecognitionActive {
print("[AzureAsrHelper] 已经在进行连续识别中,忽略请求")
return true
}
// 重置 recognizer
if !resetRecognizer() {
print("[AzureAsrHelper] 尝试重新创建识别器...")
return false
}
do {
// 激活音频会话
try audioSession.setActive(true)
// 添加识别事件处理
recognizer?.addRecognizedEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == .recognizedSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
let eventData: [String: Any] = [
"eventType": "finalResult",
"text": event.result.text ?? "",
"language": detectedLanguage
]
self.eventHandler?(eventData)
}
}
// 识别中事件
recognizer?.addRecognizingEventHandler { [weak self] _, event in
guard let self = self else { return }
if event.result.reason == .recognizingSpeech {
let detectedLanguage = self.getDetectedLanguage(from: event.result)
let eventData: [String: Any] = [
"eventType": "recognizing",
"text": event.result.text ?? "",
"language": detectedLanguage
]
self.eventHandler?(eventData)
}
}
// 会话事件
recognizer?.addSessionStartedEventHandler { [weak self] _, _ in
guard let self = self else { return }
let eventData: [String: Any] = [
"eventType": "sessionStarted"
]
self.eventHandler?(eventData)
}
recognizer?.addSessionStoppedEventHandler { [weak self] _, _ in
guard let self = self else { return }
self.isContinuousRecognitionActive = false
let eventData: [String: Any] = [
"eventType": "sessionStopped"
]
self.eventHandler?(eventData)
}
// 取消事件
recognizer?.addCanceledEventHandler { [weak self] _, event in
guard let self = self else { return }
self.isContinuousRecognitionActive = false
var errorMessage = "未知错误"
if let cancellationDetails = try? SPXCancellationDetails(fromCanceledRecognitionResult: event.result) {
errorMessage = cancellationDetails.errorDetails ?? "未知错误"
}
let eventData: [String: Any] = [
"eventType": "error",
"error": "识别取消: \(errorMessage)"
]
self.eventHandler?(eventData)
}
// 开始连续识别
try recognizer?.startContinuousRecognition()
isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别已启动")
return true
} catch {
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
return false
}
}
/// 停止连续语音识别
///
/// - Returns: 是否成功停止连续识别
func stopContinuousRecognition() -> Bool {
if !initialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
return false
}
if !isContinuousRecognitionActive {
print("[AzureAsrHelper] 未进行连续识别,忽略停止请求")
return true
}
do {
print("[AzureAsrHelper] 停止连续语音识别")
if recognizer == nil {
if isContinuousRecognitionActive {
print("[AzureAsrHelper] 警告: 识别器为空,但状态显示活跃")
}
isContinuousRecognitionActive = false
// 通知停止成功
let eventData: [String: Any] = [
"eventType": "success",
"message": "连续识别已停止"
]
eventHandler?(eventData)
return true
}
// 停止连续识别
try recognizer?.stopContinuousRecognition()
// 延迟一点时间确保处理完成
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self = self else { return }
// 重置状态
self.isContinuousRecognitionActive = false
// 恢复音频会话
do {
try self.audioSession.setActive(false, options: .notifyOthersOnDeactivation)
} catch {
// 忽略错误
}
print("[AzureAsrHelper] 连续识别已停止")
// 通知停止成功
let eventData: [String: Any] = [
"eventType": "success",
"message": "连续识别已停止"
]
self.eventHandler?(eventData)
}
return true
} catch {
// 强制重置状态
isContinuousRecognitionActive = false
print("[AzureAsrHelper] 警告: 停止连续识别失败: \(error.localizedDescription)")
// 通知停止失败,但仍然视为处理完成
let eventData: [String: Any] = [
"eventType": "success",
"message": "连续识别已停止(但有错误)"
]
eventHandler?(eventData)
return false
}
}
/// 释放资源
func dispose() {
// 如果正在进行连续识别,先停止
if isContinuousRecognitionActive {
_ = stopContinuousRecognition()
}
// 恢复音频会话
do {
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
} catch {
// 忽略错误
}
// 释放资源
recognizer = nil
speechConfig = nil
audioConfig = nil
initialized = false
isContinuousRecognitionActive = false
print("[AzureAsrHelper] 资源已释放")
}
/// 检查连续识别是否处于活跃状态
///
/// - Returns: 是否正在进行连续识别
func isContinuousRecognitionActive() -> Bool {
return isContinuousRecognitionActive
}
}

182
local_plugins/azure_speech/ios/Classes/AzureSpeechPlugin.swift

@ -0,0 +1,182 @@
import Flutter
import UIKit
public class AzureSpeechPlugin: NSObject, FlutterPlugin {
private var ttsHelper: AzureTtsHelper?
private var asrHelper: AzureAsrHelper?
private var eventSink: FlutterEventSink?
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "azure_speech", binaryMessenger: registrar.messenger())
let instance = AzureSpeechPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
// 初始化事件通道
let eventChannel = FlutterEventChannel(name: "azure_speech/asr_events", binaryMessenger: registrar.messenger())
eventChannel.setStreamHandler(AsrStreamHandler(instance: instance))
}
override init() {
super.init()
ttsHelper = AzureTtsHelper()
asrHelper = AzureAsrHelper()
// 设置ASR事件处理
asrHelper?.setEventHandler { [weak self] event in
self?.handleAsrEvent(event)
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
// TTS相关方法
case "initializeTts":
guard let args = call.arguments as? [String: Any],
let subscriptionKey = args["subscriptionKey"] as? String,
let serviceRegion = args["serviceRegion"] as? String else {
result(false)
return
}
let language = args["language"] as? String ?? "zh-CN"
let success = ttsHelper?.initialize(speechSubscriptionKey: subscriptionKey, serviceRegion: serviceRegion, language: language) ?? false
result(success)
case "setTtsVoice":
guard let args = call.arguments as? [String: Any],
let voiceName = args["voiceName"] as? String else {
result(false)
return
}
let success = ttsHelper?.setVoice(voiceName: voiceName) ?? false
result(success)
case "setTtsSpeechParams":
guard let args = call.arguments as? [String: Any],
let rate = args["rate"] as? Int,
let pitch = args["pitch"] as? Int,
let volume = args["volume"] as? Int else {
result(false)
return
}
let success = ttsHelper?.setSpeechParams(rate: rate, pitch: pitch, volume: volume) ?? false
result(success)
case "speakText":
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(false)
return
}
ttsHelper?.speakText(text: text) { success, _ in
result(success)
}
case "stopSpeaking":
let success = ttsHelper?.stopSpeaking() ?? false
result(success)
case "isSpeaking":
let speaking = ttsHelper?.isSpeaking() ?? false
result(speaking)
case "setTtsAudioOutputType":
guard let args = call.arguments as? [String: Any],
let outputType = args["outputType"] as? String else {
result(false)
return
}
var type: AzureTtsHelper.AudioOutputType = .auto
switch outputType.uppercased() {
case "SPEAKER":
type = .speaker
case "EARPIECE":
type = .earpiece
default:
type = .auto
}
let success = ttsHelper?.setAudioOutputType(outputType: type) ?? false
result(success)
// ASR相关方法
case "initializeAsr":
guard let args = call.arguments as? [String: Any],
let subscriptionKey = args["subscriptionKey"] as? String,
let serviceRegion = args["serviceRegion"] as? String,
let supportedLanguages = args["supportedLanguages"] as? [String] else {
result(false)
return
}
let success = asrHelper?.initialize(speechSubscriptionKey: subscriptionKey, serviceRegion: serviceRegion, supportedLanguages: supportedLanguages) ?? false
result(success)
case "recognizeOnce":
asrHelper?.recognizeOnce { success, text, language, error in
var resultMap: [String: Any] = ["success": success]
if success {
resultMap["text"] = text
resultMap["language"] = language
} else {
resultMap["error"] = error
}
result(resultMap)
}
case "startContinuousRecognition":
let success = asrHelper?.startContinuousRecognition() ?? false
result(success)
case "stopContinuousRecognition":
let success = asrHelper?.stopContinuousRecognition() ?? false
result(success)
case "isContinuousRecognitionActive":
let isActive = asrHelper?.isContinuousRecognitionActive() ?? false
result(isActive)
case "dispose":
ttsHelper?.dispose()
asrHelper?.dispose()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
// 设置事件接收器
func setEventSink(_ sink: FlutterEventSink?) {
self.eventSink = sink
}
// 处理ASR事件
private func handleAsrEvent(_ event: [String: Any]) {
self.eventSink?(event)
}
}
// ASR事件流处理器
class AsrStreamHandler: NSObject, FlutterStreamHandler {
private weak var plugin: AzureSpeechPlugin?
init(instance: AzureSpeechPlugin) {
self.plugin = instance
super.init()
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
plugin?.setEventSink(events)
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
plugin?.setEventSink(nil)
return nil
}
}

330
local_plugins/azure_speech/ios/Classes/AzureTtsHelper.swift

@ -0,0 +1,330 @@
import Foundation
import AVFoundation
import MicrosoftCognitiveServicesSpeech
/// Azure 语音合成辅助类
class AzureTtsHelper: NSObject {
private var synthesizer: SPXSpeechSynthesizer?
private var speechConfig: SPXSpeechConfig?
private var audioConfig: SPXAudioConfig?
private var initialized = false
private var speaking = false
// 音频输出类型
enum AudioOutputType {
case speaker // 扬声器
case earpiece // 听筒
case auto // 自动选择
}
// 当前设置
private var currentVoiceName = "zh-CN-XiaoxiaoNeural"
private var currentSpeechRate = 0
private var currentPitch = 0
private var currentVolume = 100
private var currentAudioOutputType: AudioOutputType = .auto
// 音频会话管理
private let audioSession = AVAudioSession.sharedInstance()
/// 初始化 TTS 引擎
///
/// - Parameters:
/// - speechSubscriptionKey: Azure 语音服务订阅密钥
/// - serviceRegion: Azure 语音服务区域
/// - language: 可选,默认语言,默认为 "zh-CN"
/// - Returns: 是否初始化成功
func initialize(speechSubscriptionKey: String, serviceRegion: String, language: String = "zh-CN") -> Bool {
print("[AzureTtsHelper] 初始化 Azure 语音服务")
// 检查配置是否为空
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
return false
}
// 释放之前的资源
dispose()
do {
// 创建语音配置
speechConfig = try SPXSpeechConfig(subscription: speechSubscriptionKey, region: serviceRegion)
// 设置语音合成输出格式为高质量音频
speechConfig?.setSpeechSynthesisOutputFormat(.riff24Khz16BitMonoPcm)
// 设置默认语言
speechConfig?.setSpeechSynthesisLanguage(language)
// 设置默认语音
speechConfig?.setSpeechSynthesisVoiceName(currentVoiceName)
// 创建音频配置 - 使用默认扬声器
audioConfig = SPXAudioConfig.default()
// 创建语音合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig: speechConfig!, audioConfig: audioConfig!)
initialized = true
// 设置默认音频输出类型为自动
setAudioOutputType(outputType: .auto)
print("[AzureTtsHelper] TTS 引擎初始化成功")
return true
} catch {
print("[AzureTtsHelper] TTS 引擎初始化失败: \(error.localizedDescription)")
return false
}
}
/// 设置音频输出设备类型
///
/// - Parameter outputType: 音频输出设备类型
/// - Returns: 是否设置成功
func setAudioOutputType(outputType: AudioOutputType) -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
return false
}
do {
currentAudioOutputType = outputType
switch outputType {
case .speaker:
// 使用扬声器
try audioSession.setCategory(.playback, mode: .default)
try audioSession.overrideOutputAudioPort(.speaker)
print("[AzureTtsHelper] 已设置音频输出设备为扬声器")
case .earpiece:
// 使用听筒
try audioSession.setCategory(.playback, mode: .voiceChat)
try audioSession.overrideOutputAudioPort(.none)
print("[AzureTtsHelper] 已设置音频输出设备为听筒")
case .auto:
// 检查是否有耳机连接
let outputs = audioSession.currentRoute.outputs
let hasHeadphones = outputs.contains { output in
return output.portType == .headphones || output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP
}
if hasHeadphones {
// 有耳机,使用耳机
try audioSession.setCategory(.playback, mode: .default)
try audioSession.overrideOutputAudioPort(.none)
print("[AzureTtsHelper] 已设置音频输出设备为耳机")
} else {
// 无耳机,使用听筒
try audioSession.setCategory(.playback, mode: .voiceChat)
try audioSession.overrideOutputAudioPort(.none)
print("[AzureTtsHelper] 已设置音频输出设备为听筒")
}
}
try audioSession.setActive(true)
return true
} catch {
print("[AzureTtsHelper] 设置音频输出设备失败: \(error.localizedDescription)")
return false
}
}
/// 设置语音
///
/// - Parameter voiceName: 语音名称,例如 "zh-CN-XiaoxiaoNeural"
/// - Returns: 是否设置成功
func setVoice(voiceName: String) -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
return false
}
if voiceName == currentVoiceName {
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
return true
}
do {
currentVoiceName = voiceName
speechConfig?.setSpeechSynthesisVoiceName(voiceName)
// 重新创建合成器
synthesizer = try SPXSpeechSynthesizer(speechConfig: speechConfig!, audioConfig: audioConfig!)
print("[AzureTtsHelper] 已设置语音: \(voiceName)")
return true
} catch {
print("[AzureTtsHelper] 设置语音失败: \(error.localizedDescription)")
return false
}
}
/// 设置语音合成参数
///
/// - Parameters:
/// - rate: 语速,范围 -100 到 100,默认为 0
/// - pitch: 音调,范围 -100 到 100,默认为 0
/// - volume: 音量,范围 0 到 100,默认为 100
/// - Returns: 是否设置成功
func setSpeechParams(rate: Int = 0, pitch: Int = 0, volume: Int = 100) -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
return false
}
currentSpeechRate = rate
currentPitch = pitch
currentVolume = volume
print("[AzureTtsHelper] 已设置语音参数: 语速=\(rate), 音调=\(pitch), 音量=\(volume)")
return true
}
/// 合成文本为语音并播放
///
/// - Parameters:
/// - text: 要合成的文本
/// - completion: 完成回调,返回是否成功和可能的错误信息
func speakText(text: String, completion: @escaping (Bool, String?) -> Void) {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
completion(false, "TTS 引擎尚未初始化")
return
}
do {
print("[AzureTtsHelper] 开始合成文本: \(text)")
// 生成 SSML
let ssml = generateSsml(text: text)
// 使用 SSML 合成语音
speakSsml(ssml: ssml, completion: completion)
} catch {
print("[AzureTtsHelper] 语音合成异常: \(error.localizedDescription)")
completion(false, "语音合成异常: \(error.localizedDescription)")
}
}
/// 生成 SSML 文本
///
/// - Parameter text: 要转换的文本
/// - Returns: SSML 格式的文本
private func generateSsml(text: String) -> String {
// 计算 SSML 参数
let rateParam = currentSpeechRate == 0 ? "0%" : (currentSpeechRate < 0 ? "\(Int(Double(currentSpeechRate) * 0.9))%" : "\(currentSpeechRate)%")
let pitchParam = currentPitch == 0 ? "0%" : "\(Int(Double(currentPitch) * 0.5))%"
let volumeParam = "\(min(max(currentVolume, 0), 100))%"
return """
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="zh-CN">
<voice name="\(currentVoiceName)">
<prosody rate="\(rateParam)" pitch="\(pitchParam)" volume="\(volumeParam)">
\(text)
</prosody>
</voice>
</speak>
"""
}
/// 合成 SSML 为语音并播放
///
/// - Parameters:
/// - ssml: SSML 格式的文本
/// - completion: 完成回调,返回是否成功和可能的错误信息
private func speakSsml(ssml: String, completion: @escaping (Bool, String?) -> Void) {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
completion(false, "TTS 引擎尚未初始化")
return
}
do {
print("[AzureTtsHelper] 开始合成 SSML")
// 标记为正在播放
speaking = true
// 激活音频会话
try audioSession.setActive(true)
// 异步合成语音
let result = try synthesizer!.speakSsml(ssml)
switch result.reason {
case .synthesizingAudioCompleted:
print("[AzureTtsHelper] 语音合成完成")
speaking = false
completion(true, "语音合成完成")
case .canceled:
if let cancelDetails = try? SPXSpeechSynthesisCancellationDetails(fromResult: result) {
print("[AzureTtsHelper] 语音合成取消: \(cancelDetails.errorDetails ?? "未知错误")")
speaking = false
completion(false, "语音合成取消: \(cancelDetails.errorDetails ?? "未知错误")")
} else {
print("[AzureTtsHelper] 语音合成取消")
speaking = false
completion(false, "语音合成取消")
}
default:
print("[AzureTtsHelper] 语音合成失败: \(result.reason)")
speaking = false
completion(false, "语音合成失败: \(result.reason)")
}
} catch {
print("[AzureTtsHelper] 语音合成异常: \(error.localizedDescription)")
speaking = false
completion(false, "语音合成异常: \(error.localizedDescription)")
}
}
/// 停止当前语音合成
///
/// - Returns: 是否停止成功
func stopSpeaking() -> Bool {
if !initialized {
print("[AzureTtsHelper] TTS 引擎尚未初始化")
return false
}
do {
try synthesizer?.stopSpeaking()
speaking = false
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 停止语音合成失败: \(error.localizedDescription)")
return false
}
}
/// 释放资源
func dispose() {
do {
stopSpeaking()
// 恢复音频会话
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
synthesizer = nil
speechConfig = nil
audioConfig = nil
initialized = false
speaking = false
print("[AzureTtsHelper] TTS 引擎已释放")
} catch {
print("[AzureTtsHelper] 释放 TTS 引擎失败: \(error.localizedDescription)")
}
}
/// 检查当前是否正在播放语音
///
/// - Returns: 是否正在播放语音
func isSpeaking() -> Bool {
return speaking
}
}

24
local_plugins/azure_speech/ios/azure_speech.podspec

@ -0,0 +1,24 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint azure_speech.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'azure_speech'
s.version = '0.0.1'
s.summary = 'Azure Speech plugin'
s.description = <<-DESC
A Flutter plugin for Azure Speech services.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.dependency 'MicrosoftCognitiveServicesSpeech-iOS'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

32
local_plugins/azure_speech/pubspec.yaml

@ -0,0 +1,32 @@
name: azure_speech
description: Azure语音服务插件,包含TTS和ASR服务
version: 0.0.1
homepage:
environment:
sdk: ">=2.17.0 <3.0.0"
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
plugin:
platforms:
android:
package: com.yunqiinnovation.azure_speech
pluginClass: AzureSpeechPlugin
ios:
pluginClass: AzureSpeechPlugin

0
local_plugins/chat_storage/LICENSE

80
local_plugins/chat_storage/README.md

@ -0,0 +1,80 @@
# chat_storage_sqlite
SQLite存储插件,用于存储聊天记录并支持分页查询。
## 功能特点
- 使用SQLite数据库存储聊天记录
- 提供分页查询功能,可指定获取对应页码的记录
- 支持按会话ID组织和查询聊天记录
- 提供添加、查询、删除等基本操作
## 使用方法
### 保存聊天记录
```dart
import 'package:chat_storage/chat_storage.dart';
// 保存一条聊天记录
final result = await ChatStorage.saveMessage(
sessionId: 'session_123',
message: '你好,这是一条测试消息',
sender: 'user',
metadata: '{"timestamp": 1630000000000}' // 可选的元数据,支持JSON字符串
);
if (result['success']) {
print('消息保存成功,ID: ${result['id']}');
} else {
print('消息保存失败: ${result['error']}');
}
```
### 获取聊天记录(支持分页)
```dart
// 获取指定会话的聊天记录,支持分页
final pagedResult = await ChatStorage.getMessages(
sessionId: 'session_123',
page: 1, // 当前页码,从1开始
pageSize: 20, // 每页记录数
);
// 使用结果
print('总记录数: ${pagedResult.totalCount}');
print('总页数: ${pagedResult.totalPages}');
print('当前页: ${pagedResult.page}');
// 遍历消息列表
for (var message in pagedResult.messages) {
print('ID: ${message.id}');
print('发送者: ${message.sender}');
print('内容: ${message.message}');
print('时间: ${message.timestamp}');
print('---');
}
```
### 删除聊天记录
```dart
// 方式1:删除指定ID的消息
final deleteResult1 = await ChatStorage.deleteMessages(
messageIds: [1, 2, 3],
);
// 方式2:删除整个会话的所有消息
final deleteResult2 = await ChatStorage.deleteMessages(
sessionId: 'session_123',
);
// 方式3:清空整个数据库
final clearResult = await ChatStorage.clearDatabase();
```
## 注意事项
- 插件会自动创建和管理SQLite数据库,无需手动初始化
- 所有操作都是异步的,请使用async/await处理返回结果
- 插件内部已处理异常情况,但建议在使用时添加适当的错误处理

60
local_plugins/chat_storage/android/build.gradle.kts

@ -0,0 +1,60 @@
plugins {
// Android Library 插件
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.chat_storage"
// 目标 SDK 版本
compileSdk = 33
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 33
}
// Java 语言级别兼容配置
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Kotlin 语言级别
kotlinOptions {
jvmTarget = "11"
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
implementation("com.fasterxml.jackson.core:jackson-databind:2.14.2")
implementation("com.squareup.okhttp3:okhttp:4.9.3")
implementation("com.anthropic:anthropic-java:0.8.0")
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
}
dependencies {
// SQLite相关依赖
implementation("androidx.sqlite:sqlite:2.3.1")
}

1
local_plugins/chat_storage/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "chat_storage"

8
local_plugins/chat_storage/android/src/main/AndroidManifest.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.chat_storage">
<!-- 添加sqlite相关权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

236
local_plugins/chat_storage/android/src/main/kotlin/com/yunqiinnovation/chat_storage/ChatStoragePlugin.kt

@ -0,0 +1,236 @@
package com.yunqiinnovation.chat_storage
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
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
import org.json.JSONArray
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
// 定义数据库帮助类
private class ChatDatabaseHelper(context: Context) :
SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "chat_messages.db"
private const val DATABASE_VERSION = 1
// 定义消息表
private const val TABLE_MESSAGES = "messages"
private const val COLUMN_ID = "id"
private const val COLUMN_SESSION_ID = "session_id"
private const val COLUMN_MESSAGE = "message"
private const val COLUMN_SENDER = "sender"
private const val COLUMN_TIMESTAMP = "timestamp"
private const val COLUMN_METADATA = "metadata"
}
override fun onCreate(db: SQLiteDatabase) {
// 创建消息表
val createMessagesTable = """
CREATE TABLE $TABLE_MESSAGES (
$COLUMN_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$COLUMN_SESSION_ID TEXT NOT NULL,
$COLUMN_MESSAGE TEXT NOT NULL,
$COLUMN_SENDER TEXT NOT NULL,
$COLUMN_TIMESTAMP TEXT NOT NULL,
$COLUMN_METADATA TEXT
)
""".trimIndent()
db.execSQL(createMessagesTable)
// 创建索引以加快查询速度
db.execSQL("CREATE INDEX idx_session_id ON $TABLE_MESSAGES ($COLUMN_SESSION_ID)")
db.execSQL("CREATE INDEX idx_timestamp ON $TABLE_MESSAGES ($COLUMN_TIMESTAMP)")
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
// 处理数据库升级逻辑
if (oldVersion < newVersion) {
// 简单的升级策略 - 删除旧表并创建新表
// 在实际应用中,应该实现数据迁移而非删除
db.execSQL("DROP TABLE IF EXISTS $TABLE_MESSAGES")
onCreate(db)
}
}
}
/** ChatStorageSqlitePlugin */
class ChatStoragePlugin: FlutterPlugin, MethodCallHandler {
private lateinit var channel : MethodChannel
private lateinit var context: Context
private lateinit var dbHelper: ChatDatabaseHelper
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "chat_storage")
channel.setMethodCallHandler(this)
dbHelper = ChatDatabaseHelper(context)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"saveMessage" -> {
try {
val sessionId = call.argument<String>("sessionId") ?: ""
val message = call.argument<String>("message") ?: ""
val sender = call.argument<String>("sender") ?: ""
val metadata = call.argument<String>("metadata")
val db = dbHelper.writableDatabase
// 获取当前时间作为时间戳
val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())
// 执行插入操作
val values = android.content.ContentValues().apply {
put("session_id", sessionId)
put("message", message)
put("sender", sender)
put("timestamp", timestamp)
put("metadata", metadata)
}
val id = db.insert("messages", null, values)
if (id != -1L) {
result.success(mapOf("success" to true, "id" to id))
} else {
result.error("DB_ERROR", "插入消息失败", null)
}
} catch (e: Exception) {
result.error("EXCEPTION", "保存消息时发生错误: ${e.message}", null)
}
}
"getMessages" -> {
try {
val sessionId = call.argument<String>("sessionId") ?: ""
val page = call.argument<Int>("page") ?: 1
val pageSize = call.argument<Int>("pageSize") ?: 20
val db = dbHelper.readableDatabase
val offset = (page - 1) * pageSize
// 构建查询语句
val query = """
SELECT * FROM messages
WHERE session_id = ?
ORDER BY timestamp DESC
LIMIT ? OFFSET ?
""".trimIndent()
val selectionArgs = arrayOf(sessionId, pageSize.toString(), offset.toString())
val cursor = db.rawQuery(query, selectionArgs)
val messages = cursorToJsonArray(cursor)
// 获取总记录数
val countQuery = "SELECT COUNT(*) FROM messages WHERE session_id = ?"
val countCursor = db.rawQuery(countQuery, arrayOf(sessionId))
var totalCount = 0
if (countCursor.moveToFirst()) {
totalCount = countCursor.getInt(0)
}
countCursor.close()
// 计算总页数
val totalPages = if (totalCount % pageSize == 0) {
totalCount / pageSize
} else {
(totalCount / pageSize) + 1
}
val response = JSONObject().apply {
put("messages", messages)
put("page", page)
put("pageSize", pageSize)
put("totalCount", totalCount)
put("totalPages", totalPages)
}
result.success(response.toString())
} catch (e: Exception) {
result.error("EXCEPTION", "获取消息时发生错误: ${e.message}", null)
}
}
"deleteMessages" -> {
try {
val sessionId = call.argument<String>("sessionId") ?: ""
val messageIds = call.argument<List<Int>>("messageIds")
val db = dbHelper.writableDatabase
if (messageIds != null && messageIds.isNotEmpty()) {
// 删除指定ID的消息
val idsStr = messageIds.joinToString(",")
val deleteQuery = "DELETE FROM messages WHERE id IN ($idsStr)"
db.execSQL(deleteQuery)
result.success(mapOf("success" to true, "count" to messageIds.size))
} else if (sessionId.isNotEmpty()) {
// 删除会话中的所有消息
val deleteQuery = "DELETE FROM messages WHERE session_id = ?"
db.execSQL(deleteQuery, arrayOf(sessionId))
result.success(mapOf("success" to true))
} else {
result.error("INVALID_ARGS", "必须提供sessionId或messageIds", null)
}
} catch (e: Exception) {
result.error("EXCEPTION", "删除消息时发生错误: ${e.message}", null)
}
}
"clearDatabase" -> {
try {
val db = dbHelper.writableDatabase
db.execSQL("DELETE FROM messages")
result.success(mapOf("success" to true))
} catch (e: Exception) {
result.error("EXCEPTION", "清空数据库时发生错误: ${e.message}", null)
}
}
else -> {
result.notImplemented()
}
}
}
private fun cursorToJsonArray(cursor: Cursor): JSONArray {
val jsonArray = JSONArray()
val idIndex = cursor.getColumnIndex("id")
val sessionIdIndex = cursor.getColumnIndex("session_id")
val messageIndex = cursor.getColumnIndex("message")
val senderIndex = cursor.getColumnIndex("sender")
val timestampIndex = cursor.getColumnIndex("timestamp")
val metadataIndex = cursor.getColumnIndex("metadata")
while (cursor.moveToNext()) {
val jsonObject = JSONObject().apply {
put("id", cursor.getInt(idIndex))
put("sessionId", cursor.getString(sessionIdIndex))
put("message", cursor.getString(messageIndex))
put("sender", cursor.getString(senderIndex))
put("timestamp", cursor.getString(timestampIndex))
if (metadataIndex != -1 && !cursor.isNull(metadataIndex)) {
put("metadata", cursor.getString(metadataIndex))
}
}
jsonArray.put(jsonObject)
}
cursor.close()
return jsonArray
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}

0
local_plugins/chat_storage/ios/Classes/.gitkeep

23
local_plugins/chat_storage/ios/chat_storage.podspec

@ -0,0 +1,23 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint chat_storage.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'chat_storage'
s.version = '0.0.1'
s.summary = 'Chat Storage plugin'
s.description = <<-DESC
A Flutter plugin for chat storage services.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

154
local_plugins/chat_storage/lib/chat_storage.dart

@ -0,0 +1,154 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
/// 聊天记录数据模型
class ChatMessage {
final int? id;
final String sessionId;
final String message;
final String sender;
final String timestamp;
final String? metadata;
ChatMessage({
this.id,
required this.sessionId,
required this.message,
required this.sender,
required this.timestamp,
this.metadata,
});
Map<String, dynamic> toMap() {
return {
'id': id,
'sessionId': sessionId,
'message': message,
'sender': sender,
'timestamp': timestamp,
'metadata': metadata,
};
}
factory ChatMessage.fromMap(Map<String, dynamic> map) {
return ChatMessage(
id: map['id'],
sessionId: map['sessionId'],
message: map['message'],
sender: map['sender'],
timestamp: map['timestamp'],
metadata: map['metadata'],
);
}
}
/// 分页结果数据模型
class PagedResult {
final List<ChatMessage> messages;
final int page;
final int pageSize;
final int totalCount;
final int totalPages;
PagedResult({
required this.messages,
required this.page,
required this.pageSize,
required this.totalCount,
required this.totalPages,
});
}
/// SQLite聊天记录存储插件
class ChatStorage {
static const MethodChannel _channel = MethodChannel('chat_storage');
/// 保存一条聊天记录
static Future<Map<String, dynamic>> saveMessage({
required String sessionId,
required String message,
required String sender,
String? metadata,
}) async {
try {
final result = await _channel.invokeMethod('saveMessage', {
'sessionId': sessionId,
'message': message,
'sender': sender,
});
return Map<String, dynamic>.from(result);
} catch (e) {
return {'success': false, 'error': e.toString()};
}
}
/// 获取指定会话的聊天记录(支持分页)
static Future<PagedResult> getMessages({
required String sessionId,
int page = 1,
int pageSize = 20,
}) async {
try {
final String result = await _channel.invokeMethod('getMessages', {
'sessionId': sessionId,
'page': page,
'pageSize': pageSize,
});
final Map<String, dynamic> data = jsonDecode(result);
final List<dynamic> messagesList = data['messages'];
List<ChatMessage> messages = messagesList.map((item) {
return ChatMessage.fromMap(Map<String, dynamic>.from(item));
}).toList();
return PagedResult(
messages: messages,
page: data['page'],
pageSize: data['pageSize'],
totalCount: data['totalCount'],
totalPages: data['totalPages'],
);
} catch (e) {
return PagedResult(
messages: [],
page: page,
pageSize: pageSize,
totalCount: 0,
totalPages: 0,
);
}
}
/// 删除聊天记录,可按会话ID或消息ID列表删除
static Future<Map<String, dynamic>> deleteMessages({
String? sessionId,
List<int>? messageIds,
}) async {
try {
if (sessionId == null && (messageIds == null || messageIds.isEmpty)) {
return {'success': false, 'error': '必须提供sessionId或messageIds'};
}
final result = await _channel.invokeMethod('deleteMessages', {
'sessionId': sessionId,
'messageIds': messageIds,
});
return Map<String, dynamic>.from(result);
} catch (e) {
return {'success': false, 'error': e.toString()};
}
}
/// 清空整个数据库
static Future<Map<String, dynamic>> clearDatabase() async {
try {
final result = await _channel.invokeMethod('clearDatabase');
return Map<String, dynamic>.from(result);
} catch (e) {
return {'success': false, 'error': e.toString()};
}
}
}

30
local_plugins/chat_storage/pubspec.yaml

@ -0,0 +1,30 @@
name: chat_storage
description: A Flutter plugin for storing chat messages using SQLite.
version: 0.0.1
# homepage:
environment:
sdk: ">=2.17.0 <3.0.0"
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
sqflite: ^2.3.0
path: ^1.8.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
plugin:
platforms:
android:
package: com.yunqiinnovation.chat_storage
pluginClass: ChatStoragePlugin
ios:
pluginClass: ChatStoragePlugin

144
local_plugins/jieli/README.md

@ -0,0 +1,144 @@
# Jieli 插件
一个用于连接杰理蓝牙耳机的Flutter插件,支持Android 12+的BLE通信。
## 功能
- 扫描支持特定BLE服务的耳机设备
- 连接设备并发现服务
- 接收耳机的唤醒通知
- 向耳机发送控制命令启动录音
- 接收音频数据流
## 技术规范
此插件实现了以下BLE特性:
- **服务UUID**: 0000A100-0000-1000-8000-00805F9B34FB
- **音频数据特征UUID**: 0000A101-0000-1000-8000-00805F9B34FB
- **控制特征UUID**: 0000A102-0000-1000-8000-00805F9B34FB
## 使用方法
### 初始化插件
```dart
final jieli = Jieli();
```
### 检查蓝牙状态
```dart
bool isEnabled = await jieli.isBluetoothEnabled();
if (!isEnabled) {
await jieli.requestBluetoothEnable();
}
```
### 扫描设备
```dart
// 监听扫描结果
jieli.scanResults.listen((devices) {
// 处理扫描到的设备列表
for (var device in devices) {
print('发现设备: ${device.name} (${device.id}), RSSI: ${device.rssi}');
}
});
// 开始扫描,10秒后自动停止
await jieli.startScan(scanTimeout: 10000);
// 手动停止扫描
await jieli.stopScan();
```
### 连接设备
```dart
// 监听连接状态
jieli.connectionState.listen((state) {
switch (state) {
case ConnectionState.connected:
print('设备已连接');
break;
case ConnectionState.disconnected:
print('设备已断开');
break;
case ConnectionState.connecting:
print('正在连接...');
break;
case ConnectionState.disconnecting:
print('正在断开...');
break;
case ConnectionState.error:
print('连接错误');
break;
}
});
// 连接到设备
await jieli.connect(deviceId);
// 断开连接
await jieli.disconnect();
```
### 接收唤醒信号
```dart
// 监听唤醒信号
jieli.wakeupStream.listen((wakeup) {
print('收到唤醒信号');
// 通常在收到唤醒后自动发送开始录音命令
});
```
### 发送录音命令
```dart
// 发送开始录音命令
await jieli.sendStartRecordingCommand();
// 发送停止录音命令
await jieli.sendStopRecordingCommand();
```
### 接收音频数据
```dart
// 监听音频数据流
jieli.audioDataStream.listen((data) {
print('收到音频数据: ${data.length} 字节');
// 处理音频数据
});
```
### 资源清理
```dart
// 在不再需要插件时释放资源
await jieli.dispose();
```
## 权限
使用此插件需要以下Android权限:
- Android 12+ (API 31+): `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`
- Android 11及以下: `BLUETOOTH`, `BLUETOOTH_ADMIN`
插件会在需要时自动请求权限,但建议在应用清单文件中声明这些权限:
```xml
<!-- 针对 Android 11 及以下的传统蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Android 12+ 新的 BLE 扫描和连接权限 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
```

55
local_plugins/jieli/android/build.gradle.kts

@ -0,0 +1,55 @@
plugins {
// Android Library 插件
id("com.android.library")
// Kotlin Android 插件
id("org.jetbrains.kotlin.android")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
android {
// 命名空间,对应你插件的包名(需与代码内包名保持一致)
namespace = "com.yunqiinnovation.jieli"
// 目标 SDK 版本
compileSdk = 33
defaultConfig {
// 最低 SDK 版本
minSdk = 21
targetSdk = 33
}
// Java 语言级别兼容配置
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
// Kotlin 语言级别
kotlinOptions {
jvmTarget = "11"
}
// 添加packaging配置,排除冲突文件
packaging {
resources {
excludes.add("META-INF/DEPENDENCIES")
excludes.add("META-INF/LICENSE")
excludes.add("META-INF/LICENSE.txt")
excludes.add("META-INF/license.txt")
excludes.add("META-INF/NOTICE")
excludes.add("META-INF/NOTICE.txt")
excludes.add("META-INF/notice.txt")
excludes.add("META-INF/*.kotlin_module")
}
}
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.4.0")
implementation("androidx.lifecycle:lifecycle-service:2.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
}

1
local_plugins/jieli/android/settings.gradle.kts

@ -0,0 +1 @@
rootProject.name = "jieli"

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save