liwei1dao 1 year ago
parent
commit
15f4bbbb3f
  1. 6
      .gitignore
  2. 10
      .mcp copy.json
  3. 10
      .metadata
  4. 8
      GoogleService-Info.plist
  5. 1
      amap_flutter/android/settings.gradle
  6. 3
      amap_flutter/android/src/main/AndroidManifest.xml
  7. 169
      amap_flutter/android/src/main/java/com/example/amap_flutter/AmapFlutterPlugin.java
  8. 36
      amap_flutter/lib/amap_flutter.dart
  9. 45
      amap_flutter/lib/amap_flutter_method_channel.dart
  10. 49
      amap_flutter/lib/amap_flutter_platform_interface.dart
  11. 20
      android/app/build.gradle.kts
  12. 2
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  13. 2
      android/build.gradle.kts
  14. 5
      android/settings.gradle.kts
  15. 36
      assets/config/GoogleService-Info.plist
  16. 8
      ios/Podfile
  17. 210
      ios/Podfile.lock
  18. 232
      ios/Runner.xcodeproj/project.pbxproj
  19. 123
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  20. 123
      ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
  21. 730
      ios/Runner/AppDelegate.swift
  22. 589
      ios/Runner/AudioSessionManager.swift
  23. 813
      ios/Runner/AzureAsrHelper.swift
  24. 454
      ios/Runner/AzureTtsHelper.swift
  25. 13
      ios/Runner/Base.lproj/Main.storyboard
  26. 464
      ios/Runner/BluetoothMediaButtonHelper.swift
  27. 243
      ios/Runner/ClassicBluetoothHelper.swift
  28. 118
      ios/Runner/Info.plist
  29. 0
      ios/Runner/Resources/.gitkeep
  30. BIN
      ios/Runner/Resources/sample.mp3
  31. BIN
      ios/Runner/Resources/silence.mp3
  32. 6
      ios/Runner/Runner-Bridging-Header.h
  33. 5
      ios/Runner/Runner.entitlements
  34. 565
      ios/Runner/VoiceInteractionService.swift
  35. 391
      ios/Runner/VolcanoAIService.swift
  36. BIN
      ios/Runner/sample.mp3
  37. BIN
      ios/Runner/silence.mp3
  38. 34
      lib/core/bindings/initial_binding.dart
  39. 4
      lib/core/themes/theme_service.dart
  40. 6
      lib/core/translations/language/zh_cn.dart
  41. 85
      lib/core/utils/logger.dart
  42. 48
      lib/data/services/ble_manager.dart
  43. 8
      lib/data/services/bluetooth_manager.dart
  44. 2
      lib/data/services/chat_history_service.dart
  45. 4
      lib/data/services/deapsound_ai_service.dart
  46. 6
      lib/data/services/kouzi_ai_service.dart
  47. 4
      lib/data/services/login_service.dart
  48. 10
      lib/data/services/music_manager.dart
  49. 41
      lib/data/services/navigation_manager.dart
  50. 10
      lib/data/services/network/api.dart
  51. 2
      lib/data/services/network/dio_manager.dart
  52. 4
      lib/data/services/speech_factory.dart
  53. 21
      lib/data/services/speech_impl/azure_asr_service.dart
  54. 22
      lib/data/services/speech_impl/azure_tts_service.dart
  55. 18
      lib/data/services/speech_impl/flutter_asr_service.dart
  56. 24
      lib/data/services/speech_impl/flutter_tts_service.dart
  57. 22
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  58. 38
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  59. 16
      lib/data/services/speech_impl/volcano_tts_api_service.dart
  60. 4
      lib/data/services/speech_impl/volcano_tts_service.dart
  61. 20
      lib/data/services/speech_impl/xunfei_asr_service.dart
  62. 18
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  63. 12
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  64. 4
      lib/firebase_options.dart
  65. 4
      lib/main.dart
  66. 314
      lib/modules/agent/controllers/agent_controller.dart
  67. 86
      lib/modules/agent/views/agent_view.dart
  68. 1061
      lib/modules/agent/views/message_bubble.dart
  69. 261
      lib/modules/agent/views/music_card.dart
  70. 157
      lib/modules/agent/views/navigation_card.dart
  71. 211
      lib/modules/agent/views/travel_card.dart
  72. 313
      lib/modules/agent/views/weather_card.dart
  73. 24
      lib/modules/chat/controllers/chat_controller.dart
  74. 14
      lib/modules/common/webview_page.dart
  75. 3
      lib/modules/home/controllers/home_controller.dart
  76. 11
      lib/modules/jieli_test/bindings/jieli_test_binding.dart
  77. 323
      lib/modules/jieli_test/controllers/jieli_test_controller.dart
  78. 568
      lib/modules/jieli_test/views/jieli_test_view.dart
  79. 69
      lib/modules/meeting/controllers/meeting_details_controller.dart
  80. 388
      lib/modules/meeting/views/meeting_details_view.dart
  81. 101
      lib/modules/meeting/views/mind_map_tab.dart
  82. 573
      lib/modules/meeting/views/speaker_all_bottom_sheet.dart
  83. 15
      lib/modules/meeting/views/speaker_bottom_sheet.dart
  84. 69
      lib/modules/meeting/views/speech_tab.dart
  85. 215
      lib/modules/meeting/views/summary_tab.dart
  86. 9
      lib/modules/navigation/bindings/navigation_binding.dart
  87. 5
      lib/modules/navigation/controllers/navigation_controller.dart
  88. 17
      lib/modules/navigation/views/navigation_view.dart
  89. 124
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  90. 270
      lib/modules/pairing/controllers/pairing_controller.dart
  91. 43
      lib/modules/profile/controllers/profile_controller.dart
  92. 78
      lib/modules/settings/controllers/settings_controller.dart
  93. 324
      lib/modules/settings/views/permissions_view.dart
  94. 92
      lib/modules/settings/views/settings_view.dart
  95. 18
      lib/modules/speech_test/controllers/speech_test_controller.dart
  96. 23
      lib/modules/splash/controllers/splash_controller.dart
  97. 25
      lib/modules/translation/controllers/translation_controller.dart
  98. 8
      lib/modules/translation/controllers/translation_detail_controller.dart
  99. 25
      lib/routes/app_pages.dart
  100. 2
      lib/routes/app_routes.dart

6
.gitignore

@ -136,3 +136,9 @@ coverage/
*.js_
*.js.deps
*.js.map
linux/flutter/generated_plugin_registrant.cc
linux/flutter/generated_plugin_registrant.h
linux/flutter/generated_plugins.cmake
windows/flutter/generated_plugin_registrant.cc
windows/flutter/generated_plugin_registrant.h
windows/flutter/generated_plugins.cmake

10
.mcp copy.json

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

10
.metadata

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
channel: "stable"
project_type: app
@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
- platform: ios
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
# User provided section

8
GoogleService-Info.plist

@ -3,9 +3,9 @@
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com</string>
<string>907754492821-tl2blr39g82re40sttkve36ovt6qcu6c.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
<string>com.googleusercontent.apps.907754492821-tl2blr39g82re40sttkve36ovt6qcu6c</string>
<key>ANDROID_CLIENT_ID</key>
<string>907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com</string>
<key>API_KEY</key>
@ -15,7 +15,7 @@
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.yunqiinnovation.deepsound</string>
<string>com.chuangxiangyunqi.deapsound</string>
<key>PROJECT_ID</key>
<string>deepsound-e0e3d</string>
<key>STORAGE_BUCKET</key>
@ -31,6 +31,6 @@
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:907754492821:ios:246794fc44b23174145c9c</string>
<string>1:907754492821:ios:e4b4f3b37e58a7d1145c9c</string>
</dict>
</plist>

1
amap_flutter/android/settings.gradle

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

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

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

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

@ -1,169 +0,0 @@
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("city", amapLocation.getCity());
locationData.put("address", amapLocation.getAddress());
eventSink.success(locationData);
}
}
}
};
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
methodChannel.setMethodCallHandler(null);
eventChannel.setStreamHandler(null);
destroyLocationClient();
}
}

36
amap_flutter/lib/amap_flutter.dart

@ -1,36 +0,0 @@
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

@ -1,45 +0,0 @@
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

@ -1,49 +0,0 @@
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.');
}
}

20
android/app/build.gradle.kts

@ -120,28 +120,8 @@ android {
}
dependencies {
implementation("io.modelcontextprotocol:kotlin-sdk:0.4.0")
// 添加本地插件模块依赖
implementation(project(":spotify_sdk"))
implementation(project(":deepvoice_utils"))
implementation(project(":ble_service"))
// 添加OkHttp依赖
implementation("com.squareup.okhttp3:okhttp:4.9.3")
// 添加核心库反糖化
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3")
// 添加androidx.media依赖
implementation("androidx.media:media:1.6.0")
// 添加legacy-support-v4依赖
implementation("androidx.legacy:legacy-support-v4:1.0.0")
}
flutter {

2
android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt

@ -12,7 +12,7 @@ import io.flutter.embedding.android.FlutterActivity
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
class MainActivity: FlutterActivity() {
private val TAG = "MainActivity"

2
android/build.gradle.kts

@ -35,7 +35,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.10")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20")
// 其他依赖...
}
}

5
android/settings.gradle.kts

@ -37,9 +37,9 @@ include(":chat_storage")
include(":jieli")
include(":spotify_sdk")
include(":ble_service")
include(":deepvoice_utils")
include(":classic_bluetooth")
include(":agent_service")
include(":speech")
// 设置azure_speech项目的路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":open_ai_service").projectDir = file("../local_plugins/open_ai_service/android")
@ -47,7 +47,6 @@ project(":chat_storage").projectDir = file("../local_plugins/chat_storage/androi
project(":jieli").projectDir = file("../local_plugins/jieli/android")
project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android")
project(":ble_service").projectDir = file("../local_plugins/ble_service/android")
project(":deepvoice_utils").projectDir = file("../local_plugins/deepvoice_utils/android")
project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android")
project(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android")

36
assets/config/GoogleService-Info.plist

@ -0,0 +1,36 @@
<?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>CLIENT_ID</key>
<string>907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
<key>ANDROID_CLIENT_ID</key>
<string>907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com</string>
<key>API_KEY</key>
<string>AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808</string>
<key>GCM_SENDER_ID</key>
<string>907754492821</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.yunqiinnovation.deepsound</string>
<key>PROJECT_ID</key>
<string>deepsound-e0e3d</string>
<key>STORAGE_BUCKET</key>
<string>deepsound-e0e3d.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:907754492821:ios:246794fc44b23174145c9c</string>
</dict>
</plist>

8
ios/Podfile

@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '13.0'
# Define a global platform for your project
platform :ios, '18.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@ -31,10 +31,6 @@ target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# 添加 Microsoft Cognitive Services Speech SDK
pod 'MicrosoftCognitiveServicesSpeech-iOS', '~> 1.34.0'
target 'RunnerTests' do
inherit! :search_paths
end

210
ios/Podfile.lock

@ -11,8 +11,45 @@ PODS:
- Flutter
- audio_waveforms (0.0.1):
- Flutter
- ble_service (0.0.1):
- Flutter
- chat_storage (0.0.1):
- Flutter
- classic_bluetooth (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- FBAEMKit (17.1.0):
- FBSDKCoreKit_Basics (= 17.1.0)
- FBSDKCoreKit (17.1.0):
@ -21,36 +58,39 @@ PODS:
- FBSDKCoreKit_Basics (17.1.0)
- FBSDKLoginKit (17.1.0):
- FBSDKCoreKit (= 17.1.0)
- Firebase/Auth (11.8.0):
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/Auth (11.10.0):
- Firebase/CoreOnly
- FirebaseAuth (~> 11.8.0)
- Firebase/CoreOnly (11.8.0):
- FirebaseCore (~> 11.8.0)
- firebase_auth (5.5.1):
- Firebase/Auth (= 11.8.0)
- FirebaseAuth (~> 11.10.0)
- Firebase/CoreOnly (11.10.0):
- FirebaseCore (~> 11.10.0)
- firebase_auth (5.5.2):
- Firebase/Auth (= 11.10.0)
- firebase_core
- Flutter
- firebase_core (3.12.1):
- Firebase/CoreOnly (= 11.8.0)
- firebase_core (3.13.0):
- Firebase/CoreOnly (= 11.10.0)
- Flutter
- FirebaseAppCheckInterop (11.10.0)
- FirebaseAuth (11.8.1):
- FirebaseAppCheckInterop (11.12.0)
- FirebaseAuth (11.10.0):
- FirebaseAppCheckInterop (~> 11.0)
- FirebaseAuthInterop (~> 11.0)
- FirebaseCore (~> 11.8.0)
- FirebaseCoreExtension (~> 11.8.0)
- FirebaseCore (~> 11.10.0)
- FirebaseCoreExtension (~> 11.10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.0)
- GoogleUtilities/Environment (~> 8.0)
- GTMSessionFetcher/Core (< 5.0, >= 3.4)
- RecaptchaInterop (~> 100.0)
- FirebaseAuthInterop (11.10.0)
- FirebaseCore (11.8.1):
- FirebaseCoreInternal (~> 11.8.0)
- RecaptchaInterop (~> 101.0)
- FirebaseAuthInterop (11.12.0)
- FirebaseCore (11.10.0):
- FirebaseCoreInternal (~> 11.10.0)
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/Logger (~> 8.0)
- FirebaseCoreExtension (11.8.0):
- FirebaseCore (~> 11.8.0)
- FirebaseCoreInternal (11.8.0):
- FirebaseCoreExtension (11.10.0):
- FirebaseCore (~> 11.10.0)
- FirebaseCoreInternal (11.10.0):
- "GoogleUtilities/NSData+zlib (~> 8.0)"
- Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
@ -86,25 +126,25 @@ PODS:
- AppAuth (< 2.0, >= 1.7.3)
- GTMAppAuth (< 5.0, >= 4.1.1)
- GTMSessionFetcher/Core (~> 3.3)
- GoogleUtilities/AppDelegateSwizzler (8.0.2):
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.0.2):
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.0.2):
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.0.2):
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.0.2)":
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.0.2)
- GoogleUtilities/Reachability (8.0.2):
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GTMAppAuth (4.1.1):
@ -115,8 +155,12 @@ PODS:
- GTMSessionFetcher/Core (3.5.0)
- GTMSessionFetcher/Full (3.5.0):
- GTMSessionFetcher/Core
- haptic_feedback (0.5.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- jl_ota (0.0.1):
- Flutter
- just_audio (0.0.1):
- Flutter
- FlutterMacOS
@ -135,13 +179,27 @@ PODS:
- Mantle (2.2.0):
- Mantle/extobjc (= 2.2.0)
- Mantle/extobjc (2.2.0)
- MicrosoftCognitiveServicesSpeech-iOS (1.34.0)
- music_service (0.0.1):
- Flutter
- open_ai_service (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- RecaptchaInterop (100.0.0)
- QCloudCore (6.4.7):
- QCloudCore/Default (= 6.4.7)
- QCloudCore/Default (6.4.7):
- QCloudTrack/Beacon (= 6.4.7)
- QCloudCOSXML (6.4.7):
- QCloudCOSXML/Default (= 6.4.7)
- QCloudCOSXML/Default (6.4.7):
- QCloudCore (= 6.4.7)
- QCloudTrack/Beacon (6.4.7)
- RecaptchaInterop (101.0.0)
- record_darwin (1.0.0):
- Flutter
- SDWebImage (5.21.0):
@ -156,17 +214,30 @@ PODS:
- Flutter
- FlutterMacOS
- Try
- spotify_sdk (0.0.1):
- Flutter
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- tencentcloud_cos_sdk_plugin (1.2.3):
- Flutter
- QCloudCOSXML (= 6.4.7)
- Try (2.1.1)
- webview_flutter_wkwebview (0.0.1):
- Flutter
- FlutterMacOS
- WechatOpenSDK-XCFramework (2.0.4)
DEPENDENCIES:
- app_settings (from `.symlinks/plugins/app_settings/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- audio_waveforms (from `.symlinks/plugins/audio_waveforms/ios`)
- ble_service (from `.symlinks/plugins/ble_service/ios`)
- chat_storage (from `.symlinks/plugins/chat_storage/ios`)
- classic_bluetooth (from `.symlinks/plugins/classic_bluetooth/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_auth (from `.symlinks/plugins/firebase_auth/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- Flutter (from `Flutter`)
@ -178,19 +249,28 @@ DEPENDENCIES:
- flutter_tts (from `.symlinks/plugins/flutter_tts/ios`)
- fluwx (from `.symlinks/plugins/fluwx/ios`)
- google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`)
- haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- jl_ota (from `.symlinks/plugins/jl_ota/ios`)
- just_audio (from `.symlinks/plugins/just_audio/darwin`)
- MicrosoftCognitiveServicesSpeech-iOS (~> 1.34.0)
- music_service (from `.symlinks/plugins/music_service/ios`)
- open_ai_service (from `.symlinks/plugins/open_ai_service/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- record_darwin (from `.symlinks/plugins/record_darwin/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- speech_to_text (from `.symlinks/plugins/speech_to_text/darwin`)
- spotify_sdk (from `.symlinks/plugins/spotify_sdk/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- tencentcloud_cos_sdk_plugin (from `.symlinks/plugins/tencentcloud_cos_sdk_plugin/ios`)
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`)
SPEC REPOS:
trunk:
- AppAuth
- DKImagePickerController
- DKPhotoGallery
- FBAEMKit
- FBSDKCoreKit
- FBSDKCoreKit_Basics
@ -208,10 +288,13 @@ SPEC REPOS:
- GTMSessionFetcher
- libwebp
- Mantle
- MicrosoftCognitiveServicesSpeech-iOS
- QCloudCore
- QCloudCOSXML
- QCloudTrack
- RecaptchaInterop
- SDWebImage
- SDWebImageWebPCoder
- SwiftyGif
- Try
- WechatOpenSDK-XCFramework
@ -222,8 +305,16 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/audio_session/ios"
audio_waveforms:
:path: ".symlinks/plugins/audio_waveforms/ios"
ble_service:
:path: ".symlinks/plugins/ble_service/ios"
chat_storage:
:path: ".symlinks/plugins/chat_storage/ios"
classic_bluetooth:
:path: ".symlinks/plugins/classic_bluetooth/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_auth:
:path: ".symlinks/plugins/firebase_auth/ios"
firebase_core:
@ -246,10 +337,20 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluwx/ios"
google_sign_in_ios:
:path: ".symlinks/plugins/google_sign_in_ios/darwin"
haptic_feedback:
:path: ".symlinks/plugins/haptic_feedback/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
jl_ota:
:path: ".symlinks/plugins/jl_ota/ios"
just_audio:
:path: ".symlinks/plugins/just_audio/darwin"
music_service:
:path: ".symlinks/plugins/music_service/ios"
open_ai_service:
:path: ".symlinks/plugins/open_ai_service/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
@ -260,28 +361,40 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
speech_to_text:
:path: ".symlinks/plugins/speech_to_text/darwin"
spotify_sdk:
:path: ".symlinks/plugins/spotify_sdk/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
tencentcloud_cos_sdk_plugin:
:path: ".symlinks/plugins/tencentcloud_cos_sdk_plugin/ios"
webview_flutter_wkwebview:
:path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
SPEC CHECKSUMS:
app_settings: 5127ae0678de1dcc19f2293271c51d37c89428b2
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
audio_session: 9bb7f6c970f21241b19f5a3658097ae459681ba0
audio_waveforms: a6dde7fe7c0ea05f06ffbdb0f7c1b2b2ba6cedcf
ble_service: 068ebe51800424f980282b16cbf757574eec4e08
chat_storage: 933a96144157a07a2fef7f6acb28c78ff18465d8
classic_bluetooth: c5e5c0c86acab8dcd6b8e9cef7fca654c3518034
device_info_plus: 71ffc6ab7634ade6267c7a93088ed7e4f74e5896
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
FBAEMKit: cb719c53575a3be86ea873279f30d6a2c4e15881
FBSDKCoreKit: ecdb980a24633ccb012700299ceb16d0235e14d2
FBSDKCoreKit_Basics: 045101c4a9ef10c845347424d73a29aae02c3e43
FBSDKLoginKit: 69eb59b2f839aba635616df6e422acd0ca88030a
Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf
firebase_auth: 9ebbd83276bf977dea1c74dfc199acf9d2a2f42f
firebase_core: 8d552814f6c01ccde5d88939fced4ec26f2f5510
FirebaseAppCheckInterop: 9664c858489710f682766ef54e2b6741d3b62070
FirebaseAuth: ad59a1a7b161e75f74c39f70179d2482d40e2737
FirebaseAuthInterop: 01a804fb074424fd58b92dd50dd0272277199356
FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d
FirebaseCoreExtension: 3d3f2017a00d06e09ab4ebe065391b0bb642565e
FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 1fe1c0a7d9aaea32efe01fbea5f0ebd8d70e53a2
firebase_auth: 34c515a8fa76a4ef678ac5ad5cfa3bb3120f5ab5
firebase_core: 2d4534e7b489907dcede540c835b48981d890943
FirebaseAppCheckInterop: 73b173e5ec45192e2d522ad43f526a82ad10b852
FirebaseAuth: c4146bdfdc87329f9962babd24dae89373f49a32
FirebaseAuthInterop: b583210c039a60ed3f1e48865e1f3da44a796595
FirebaseCore: 8344daef5e2661eb004b177488d6f9f0f24251b7
FirebaseCoreExtension: 6f357679327f3614e995dc7cf3f2d600bdc774ac
FirebaseCoreInternal: ef4505d2afb1d0ebbc33162cb3795382904b5679
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_blue_plus_darwin: 09444a26fb6bdef523e55b68fc1c59af5a877ea6
flutter_facebook_auth: 8a9ad5fbfc25b77e7a53ea8986866950b27edcae
@ -292,26 +405,37 @@ SPEC CHECKSUMS:
fluwx: 6bf9c5a3a99ad31b0de137dd92370a0d10a60f4b
google_sign_in_ios: 19297361f2c51d7d8ac0201b866ef1fa5d1f94a8
GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
haptic_feedback: fd1d8509833f58f164fc12122c0357fa9b2750e0
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
jl_ota: 5fa90bf47674de0f5bce9af2219e25fa0c1425f5
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
MicrosoftCognitiveServicesSpeech-iOS: 723eccb3e459bbb530df3d9bfe84a291074b09eb
music_service: 85abecc8fa21599a06907ad50d83f48a58e0dc0d
open_ai_service: 82ea947b1a081a2a17bc0c8e5467dcbd7cf9b232
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21
QCloudCore: 49e7571984a8e5fd8f0fd58c2f3b806255796223
QCloudCOSXML: 7205e76aa9cf613468222615483c9d7c074e4298
QCloudTrack: 3b53a7fc4fe3920e407f2aa73f2452992a61f7f3
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
record_darwin: fb1f375f1d9603714f55b8708a903bbb91ffdb0a
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
speech_to_text: 9dc43a5df3cbc2813f8c7cc9bd0fbf94268ed7ac
spotify_sdk: a48400bb29f70c4fe251ebfdc9135c37097ac5ca
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
tencentcloud_cos_sdk_plugin: c3d78b2c3bae42b5a20009dc047f3f1621e6272b
Try: 5ef669ae832617b3cee58cb2c6f99fb767a4ff96
webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2
WechatOpenSDK-XCFramework: 36fb2bea0754266c17184adf4963d7e6ff98b69f
PODFILE CHECKSUM: 4f26889ae572e1722a139b651939749d40c71469
PODFILE CHECKSUM: f62382080fcf32f2d4640dc419aa07d69eff0328
COCOAPODS: 1.16.2

232
ios/Runner.xcodeproj/project.pbxproj

@ -7,25 +7,16 @@
objects = {
/* Begin PBXBuildFile section */
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */; };
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */; };
0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */; };
0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */; };
0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29682D8C25660032BFCB /* silence.mp3 */; };
0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0ADE29672D8C25660032BFCB /* sample.mp3 */; };
0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */; };
0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */; };
0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */; };
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 591293922D9524D1004A65BB /* GoogleService-Info.plist */; };
6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A9190D0838960F143BDC158 /* Pods_Runner.framework */; };
A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -52,32 +43,19 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = "<group>"; };
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = "<group>"; };
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceInteractionService.swift; sourceTree = "<group>"; };
0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolcanoAIService.swift; sourceTree = "<group>"; };
0ADE29672D8C25660032BFCB /* sample.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sample.mp3; sourceTree = "<group>"; };
0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = "<group>"; };
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = "<group>"; };
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureTtsHelper.swift; sourceTree = "<group>"; };
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = "<group>"; };
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4A9190D0838960F143BDC158 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = "<group>"; };
591293922D9524D1004A65BB /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6189535D5FBB81D8377BE874 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
77D16D761891F29239C8DF02 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
762C8874AAE15CA0CB901347 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
8C1130E20389847CBD7B1AAC /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@ -85,7 +63,12 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
B3EC1304D6F5453D58E0BDB0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -93,40 +76,50 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E1D2128FAB4726BEEED42F4C /* Pods_Runner.framework in Frameworks */,
6077538D78AAB34545D7F3DD /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
B4459155A2535A0ED1D6C7D8 /* Frameworks */ = {
B3515CD9628E3332AFC88CD4 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3406320CAE3314A73EF78043 /* Pods_RunnerTests.framework in Frameworks */,
A7986DFCFDA104638080424B /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
1ACB686EC7DC36C6E33D372F /* Pods */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
762C8874AAE15CA0CB901347 /* Pods-Runner.debug.xcconfig */,
B3EC1304D6F5453D58E0BDB0 /* Pods-Runner.release.xcconfig */,
6189535D5FBB81D8377BE874 /* Pods-Runner.profile.xcconfig */,
C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */,
742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */,
D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */,
);
path = RunnerTests;
path = Pods;
sourceTree = "<group>";
};
776E5AB1CE0A62CBDBAB3996 /* Pods */ = {
1E5DDD8701F6FB27AA502080 /* Frameworks */ = {
isa = PBXGroup;
children = (
8C1130E20389847CBD7B1AAC /* Pods-Runner.debug.xcconfig */,
77D16D761891F29239C8DF02 /* Pods-Runner.release.xcconfig */,
0906EF7A73CBA5EAFC64E05D /* Pods-Runner.profile.xcconfig */,
930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */,
02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */,
9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */,
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */,
E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */,
F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */,
);
path = Pods;
name = Frameworks;
sourceTree = "<group>";
};
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
@ -147,8 +140,8 @@
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
776E5AB1CE0A62CBDBAB3996 /* Pods */,
A5A9FDE94E78257E9A48E2E8 /* Frameworks */,
1ACB686EC7DC36C6E33D372F /* Pods */,
1E5DDD8701F6FB27AA502080 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -164,18 +157,8 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
5912938C2D9520CE004A65BB /* RunnerRelease.entitlements */,
0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */,
0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */,
0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */,
0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */,
0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */,
0ADE29672D8C25660032BFCB /* sample.mp3 */,
0ADE29682D8C25660032BFCB /* silence.mp3 */,
0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */,
0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */,
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
591293922D9524D1004A65BB /* GoogleService-Info.plist */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
@ -183,19 +166,11 @@
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */,
);
path = Runner;
sourceTree = "<group>";
};
A5A9FDE94E78257E9A48E2E8 /* Frameworks */ = {
isa = PBXGroup;
children = (
4A9190D0838960F143BDC158 /* Pods_Runner.framework */,
5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@ -203,10 +178,10 @@
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */,
D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
B4459155A2535A0ED1D6C7D8 /* Frameworks */,
B3515CD9628E3332AFC88CD4 /* Frameworks */,
);
buildRules = (
);
@ -222,15 +197,15 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */,
90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */,
D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */,
510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */,
298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@ -270,9 +245,6 @@
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
@ -297,37 +269,30 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
0ADE29692D8C25660032BFCB /* silence.mp3 in Resources */,
0ADE296A2D8C25660032BFCB /* sample.mp3 in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
591293932D9524D1004A65BB /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
1BD7F444CF127CDFCB7F9F47 /* [CP] Check Pods Manifest.lock */ = {
298589D956D542AE0EE16DAE /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
@ -346,7 +311,7 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
4A5C19D622EF675DB4056F07 /* [CP] Embed Pods Frameworks */ = {
510B02006C08E514BF31EFD9 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -363,7 +328,7 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
8A6DB356BF41E96BFD7DF109 /* [CP] Check Pods Manifest.lock */ = {
90215E32C3610039A2575557 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
@ -400,21 +365,26 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
D49299AF1487DF3783287D24 /* [CP] Copy Pods Resources */ = {
D21548F12B1535261EB87672 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@ -432,14 +402,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0ADE296D2D8C66D60032BFCB /* AzureTtsHelper.swift in Sources */,
0ADE296E2D8C66D60032BFCB /* AzureAsrHelper.swift in Sources */,
0A5EDBFC2D89C4FD00215E26 /* ClassicBluetoothHelper.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
0A5EDC042D8B544100215E26 /* BluetoothMediaButtonHelper.swift in Sources */,
0A7A32E42D8DBA6D0047F746 /* VoiceInteractionService.swift in Sources */,
0A7A32E52D8DBA6D0047F746 /* VolcanoAIService.swift in Sources */,
0ADE29702D8C6C450032BFCB /* AudioSessionManager.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -498,6 +461,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -532,16 +496,16 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 23;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -552,64 +516,53 @@
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 930FE6D58BDB20688CE0A7CF /* Pods-RunnerTests.debug.xcconfig */;
baseConfigurationReference = C15B3030DF1AED99D3829135 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 02CF74744224D1C933681603 /* Pods-RunnerTests.release.xcconfig */;
baseConfigurationReference = 742A00629970110BA5008064 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9B84B894288BF76332D3F407 /* Pods-RunnerTests.profile.xcconfig */;
baseConfigurationReference = D1CF75E5BD49BB58812D5F36 /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 42C82SZUVX;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
@ -638,6 +591,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -695,6 +649,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@ -731,21 +686,18 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 23;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -759,17 +711,16 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CURRENT_PROJECT_VERSION = 23;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 42C82SZUVX;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
IPHONEOS_DEPLOYMENT_TARGET = 18.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -812,17 +763,6 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/firebase/firebase-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 11.10.0;
};
};
/* End XCRemoteSwiftPackageReference section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

123
ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@ -1,123 +0,0 @@
{
"originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4",
"version" : "11.10.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6",
"version" : "11.10.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
"version" : "8.0.2"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
"version" : "1.69.0"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
"version" : "4.4.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
}
],
"version" : 3
}

123
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved

@ -1,123 +0,0 @@
{
"originHash" : "c63c63846d9c539229e96de38d6af51417e28c0ee9a0bc48bd0f0f19d923c329",
"pins" : [
{
"identity" : "abseil-cpp-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/abseil-cpp-binary.git",
"state" : {
"revision" : "bbe8b69694d7873315fd3a4ad41efe043e1c07c5",
"version" : "1.2024072200.0"
}
},
{
"identity" : "app-check",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/app-check.git",
"state" : {
"revision" : "61b85103a1aeed8218f17c794687781505fbbef5",
"version" : "11.2.0"
}
},
{
"identity" : "firebase-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/firebase-ios-sdk",
"state" : {
"revision" : "2e4a4daac9da51f79c131e76cb47e785dd7708c4",
"version" : "11.10.0"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleAppMeasurement.git",
"state" : {
"revision" : "defe1970a9099d47765592ebd7ac5f5e9fdb57c6",
"version" : "11.10.0"
}
},
{
"identity" : "googledatatransport",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleDataTransport.git",
"state" : {
"revision" : "617af071af9aa1d6a091d59a202910ac482128f9",
"version" : "10.1.0"
}
},
{
"identity" : "googleutilities",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/GoogleUtilities.git",
"state" : {
"revision" : "53156c7ec267db846e6b64c9f4c4e31ba4cf75eb",
"version" : "8.0.2"
}
},
{
"identity" : "grpc-binary",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/grpc-binary.git",
"state" : {
"revision" : "cc0001a0cf963aa40501d9c2b181e7fc9fd8ec71",
"version" : "1.69.0"
}
},
{
"identity" : "gtm-session-fetcher",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/gtm-session-fetcher.git",
"state" : {
"revision" : "4d70340d55d7d07cc2fdf8e8125c4c126c1d5f35",
"version" : "4.4.0"
}
},
{
"identity" : "interop-ios-for-google-sdks",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/interop-ios-for-google-sdks.git",
"state" : {
"revision" : "040d087ac2267d2ddd4cca36c757d1c6a05fdbfe",
"version" : "101.0.0"
}
},
{
"identity" : "leveldb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/leveldb.git",
"state" : {
"revision" : "a0bc79961d7be727d258d33d5a6b2f1023270ba1",
"version" : "1.22.5"
}
},
{
"identity" : "nanopb",
"kind" : "remoteSourceControl",
"location" : "https://github.com/firebase/nanopb.git",
"state" : {
"revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
"version" : "2.30910.0"
}
},
{
"identity" : "promises",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/promises.git",
"state" : {
"revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
"version" : "2.4.0"
}
},
{
"identity" : "swift-protobuf",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-protobuf.git",
"state" : {
"revision" : "d72aed98f8253ec1aa9ea1141e28150f408cf17f",
"version" : "1.29.0"
}
}
],
"version" : 3
}

730
ios/Runner/AppDelegate.swift

@ -1,743 +1,13 @@
import Flutter
import UIKit
import AVFoundation
@main
@objc class AppDelegate: FlutterAppDelegate {
private var classicBluetoothHelper: ClassicBluetoothHelper?
private var mediaButtonHelper: Any?
// 添加Azure语音服务辅助类
private var azureAsrHelper: Any?
private var azureTtsHelper: Any?
// 添加火山AI服务
private var volcanoAIService: VolcanoAIService?
// 语音交互服务
private var voiceInteractionService: VoiceInteractionService?
private var audioSessionManager: AudioSessionManager?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 获取flutter控制器
guard let controller = window?.rootViewController as? FlutterViewController else {
NSLog("AppDelegate: Flutter控制器不可用")
return false
}
// 初始化音频会话管理器
audioSessionManager = AudioSessionManager.shared
// 统一注册所有方法通道
registerMethodChannel()
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
private func registerMethodChannel() {
let controller = window?.rootViewController as? FlutterViewController
guard let controller = controller else {
NSLog("AppDelegate: FlutterViewController 不可用")
return
}
// 注册经典蓝牙方法通道
registerClassicBluetoothChannels(controller: controller)
// 仅在iOS 13.0及以上版本注册媒体按钮通道和Azure语音服务通道
if #available(iOS 13.0, *) {
// 注册媒体按钮方法通道
registerMediaButtonChannels(controller: controller)
// 注册Azure ASR方法通道
registerAzureAsrChannels(controller: controller)
// 注册Azure TTS方法通道
registerAzureTtsChannels(controller: controller)
// 注册火山AI服务方法通道
registerVolcanoAIServiceChannels(controller: controller)
// 注册语音交互服务方法通道
registerVoiceInteractionChannels(controller: controller)
}
}
// MARK: - 经典蓝牙通道
private func registerClassicBluetoothChannels(controller: FlutterViewController) {
// 初始化蓝牙辅助类
if classicBluetoothHelper == nil {
classicBluetoothHelper = ClassicBluetoothHelper()
}
guard let classicBluetoothHelper = self.classicBluetoothHelper else {
NSLog("AppDelegate: 经典蓝牙辅助实例不可用")
return
}
// 注册经典蓝牙方法通道
let classicBluetoothChannel = FlutterMethodChannel(
name: "com.deep_voice.classic_bluetooth",
binaryMessenger: controller.binaryMessenger
)
classicBluetoothChannel.setMethodCallHandler { [weak self, weak classicBluetoothHelper] (call, result) in
guard let _ = self, let classicBluetoothHelper = classicBluetoothHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "经典蓝牙辅助实例不可用", details: nil))
return
}
switch call.method {
case "isBluetoothEnabled":
result(classicBluetoothHelper.isBluetoothEnabled())
case "getConnectedHeadsetDevices":
// 获取已连接的耳机设备
classicBluetoothHelper.getConnectedHeadsetDevices { devices, error in
if let error = error {
result(FlutterError(code: "GET_DEVICES_FAILED", message: error, details: nil))
} else {
result(devices)
}
}
case "initialize":
classicBluetoothHelper.initialize()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册蓝牙流通道
let classicBluetoothStreamChannel = FlutterEventChannel(
name: "com.deep_voice.classic_bluetooth_events",
binaryMessenger: controller.binaryMessenger
)
// 创建蓝牙流处理器(作为局部变量)
let bluetoothStreamHandler = BluetoothStreamHandler(bluetoothHelper: classicBluetoothHelper)
classicBluetoothStreamChannel.setStreamHandler(bluetoothStreamHandler)
}
// MARK: - 消息通道设置
// MARK: - 蓝牙媒体按键通道
@available(iOS 13.0, *)
private func registerMediaButtonChannels(controller: FlutterViewController) {
// 初始化媒体按钮辅助类
if mediaButtonHelper == nil {
mediaButtonHelper = BluetoothMediaButtonHelper()
}
// guard let mediaButtonHelper = self.mediaButtonHelper as? BluetoothMediaButtonHelper else {
// NSLog("AppDelegate: 媒体按钮辅助实例不可用")
// return
// }
// mediaButtonHelper.startButtonListening()
// 注册媒体按钮方法通道
let mediaButtonChannel = FlutterMethodChannel(
name: "com.deep_voice.bluetooth_media_button",
binaryMessenger: controller.binaryMessenger
)
mediaButtonChannel.setMethodCallHandler { [weak self] (call, result) in
guard let _ = self, let mediaButtonHelper = self?.mediaButtonHelper as? BluetoothMediaButtonHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "媒体按钮辅助实例不可用", details: nil))
return
}
switch call.method {
case "startButtonListening":
let success = mediaButtonHelper.startButtonListening()
result(success)
case "stopButtonListening":
let success = mediaButtonHelper.stopButtonListening()
result(success)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册媒体按钮流通道
let mediaButtonStreamChannel = FlutterEventChannel(
name: "com.deep_voice.bluetooth_media_button_events",
binaryMessenger: controller.binaryMessenger
)
// 创建媒体按钮流处理器
let mediaButtonStreamHandler = MediaButtonStreamHandler(mediaButtonHelper: mediaButtonHelper as! BluetoothMediaButtonHelper)
mediaButtonStreamChannel.setStreamHandler(mediaButtonStreamHandler)
}
// 注册Azure ASR通道
@available(iOS 13.0, *)
private func registerAzureAsrChannels(controller: FlutterViewController) {
// 注册Azure ASR方法通道
let azureAsrChannel = FlutterMethodChannel(
name: "com.deep_voice.azure_asr",
binaryMessenger: controller.binaryMessenger
)
// 初始化Azure ASR辅助类
azureAsrHelper = AzureAsrHelper()
// 设置ASR方法处理器
azureAsrChannel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self,
let asrHelper = self.azureAsrHelper as? AzureAsrHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "Azure ASR辅助实例不可用", details: nil))
return
}
switch call.method {
case "initialize":
guard let args = call.arguments as? [String: Any],
let key = args["subscriptionKey"] as? String,
let region = args["region"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "初始化参数无效", details: nil))
return
}
let languages = args["supportedLanguages"] as? [String]
let success = asrHelper.initialize(speechSubscriptionKey: key, serviceRegion: region, supportedLanguages: languages)
result(success)
case "recognizeOnce":
let success = asrHelper.recognizeOnce()
result(success)
case "startContinuousRecognition":
let success = asrHelper.startContinuousRecognition()
result(success)
case "stopContinuousRecognition":
let success = asrHelper.stopContinuousRecognition()
result(success)
case "isContinuousRecognitionActive":
let isActive = asrHelper.isContinuousRecognitionActive()
result(isActive)
case "dispose":
asrHelper.dispose()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册Azure ASR流通道
let azureAsrStreamChannel = FlutterEventChannel(
name: "com.deep_voice.azure_asr_events",
binaryMessenger: controller.binaryMessenger
)
// 创建ASR流处理器
let asrStreamHandler = AsrStreamHandler(azureAsrHelper: azureAsrHelper as! AzureAsrHelper)
azureAsrStreamChannel.setStreamHandler(asrStreamHandler)
}
// 注册Azure TTS通道
@available(iOS 13.0, *)
private func registerAzureTtsChannels(controller: FlutterViewController) {
// 注册Azure TTS方法通道
let azureTtsChannel = FlutterMethodChannel(
name: "com.deep_voice.azure_tts",
binaryMessenger: controller.binaryMessenger
)
// 初始化Azure TTS辅助类
azureTtsHelper = AzureTtsHelper()
// 设置TTS方法处理器
azureTtsChannel.setMethodCallHandler { [weak self] (call, result) in
guard let self = self,
let ttsHelper = self.azureTtsHelper as? AzureTtsHelper else {
result(FlutterError(code: "UNAVAILABLE", message: "Azure TTS辅助实例不可用", details: nil))
return
}
switch call.method {
case "initialize":
guard let args = call.arguments as? [String: Any],
let key = args["subscriptionKey"] as? String,
let region = args["region"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "初始化参数无效", details: nil))
return
}
let language = args["language"] as? String ?? "zh-CN"
let success = ttsHelper.initialize(speechSubscriptionKey: key, serviceRegion: region, language: language)
result(success)
case "setVoice":
guard let args = call.arguments as? [String: Any],
let voiceName = args["voiceName"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "语音名称参数无效", details: nil))
return
}
let success = ttsHelper.setVoice(voiceName: voiceName)
result(success)
case "setSpeechParams":
guard let args = call.arguments as? [String: Any] else {
result(FlutterError(code: "INVALID_ARGS", message: "语音参数无效", details: nil))
return
}
let rate = args["rate"] as? Int ?? 0
let pitch = args["pitch"] as? Int ?? 0
let volume = args["volume"] as? Int ?? 100
let success = ttsHelper.setSpeechParams(rate: rate, pitch: pitch, volume: volume)
result(success)
case "speakText":
guard let args = call.arguments as? [String: Any],
let text = args["text"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "文本参数无效", details: nil))
return
}
let success = ttsHelper.speakText(text: text)
result(success)
case "stopSpeaking":
let success = ttsHelper.stopSpeaking()
result(success)
case "isSpeaking":
let isSpeaking = ttsHelper.isSpeaking()
result(isSpeaking)
case "dispose":
ttsHelper.dispose()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册Azure TTS流通道
let azureTtsStreamChannel = FlutterEventChannel(
name: "com.deep_voice.azure_tts_events",
binaryMessenger: controller.binaryMessenger
)
// 创建TTS流处理器
let ttsStreamHandler = TtsStreamHandler(azureTtsHelper: azureTtsHelper as! AzureTtsHelper)
azureTtsStreamChannel.setStreamHandler(ttsStreamHandler)
}
// 注册火山AI服务方法通道
@available(iOS 13.0, *)
private func registerVolcanoAIServiceChannels(controller: FlutterViewController) {
guard let volcanoAIService = volcanoAIService else {
return
}
// 注册火山AI方法通道
let volcanoAIChannel = FlutterMethodChannel(
name: "com.deep_voice.volcano_ai",
binaryMessenger: controller.binaryMessenger
)
volcanoAIChannel.setMethodCallHandler { [weak self, weak volcanoAIService] (call, result) in
guard let _ = self, let volcanoAIService = volcanoAIService else {
result(FlutterError(code: "UNAVAILABLE", message: "火山AI服务实例不可用", details: nil))
return
}
switch call.method {
case "initialize":
let apiKey = call.arguments as? String ?? ""
let success = volcanoAIService.initialize(apiKey: apiKey)
result(success)
case "sendMessage":
guard let arguments = call.arguments as? [String: Any],
let message = arguments["message"] as? String,
let systemPrompt = arguments["systemPrompt"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil))
return
}
if message.isEmpty {
result(FlutterError(code: "EMPTY_MESSAGE", message: "消息不能为空", details: nil))
return
}
// 异步发送消息
Task {
do {
// 创建用户消息
let userMessage = volcanoAIService.createUserMessage(content: message)
// 发送消息并等待响应
let response = try await volcanoAIService.sendMessage(
messages: [userMessage],
systemPrompt: systemPrompt
)
// 返回响应
DispatchQueue.main.async {
result(response)
}
} catch {
// 处理错误
DispatchQueue.main.async {
result(FlutterError(
code: "AI_ERROR",
message: "AI请求失败: \(error.localizedDescription)",
details: nil
))
}
}
}
default:
result(FlutterMethodNotImplemented)
}
}
// 注册火山AI流通道
let volcanoAIStreamChannel = FlutterEventChannel(
name: "com.deep_voice.volcano_ai_stream",
binaryMessenger: controller.binaryMessenger
)
// 创建火山AI流处理器(作为局部变量)
let volcanoAIStreamHandler = VolcanoAIStreamHandler(volcanoAIService: volcanoAIService)
volcanoAIStreamChannel.setStreamHandler(volcanoAIStreamHandler)
}
// 注册语音交互服务方法通道
@available(iOS 13.0, *)
private func registerVoiceInteractionChannels(controller: FlutterViewController) {
// 初始化语音交互服务
voiceInteractionService = VoiceInteractionService.shared
// 注册语音交互服务方法通道
let voiceInteractionChannel = FlutterMethodChannel(
name: "com.deep_voice.voice_interaction",
binaryMessenger: controller.binaryMessenger
)
voiceInteractionChannel.setMethodCallHandler { [weak self, weak voiceInteractionService] (call, result) in
guard let _ = self, let voiceInteractionService = voiceInteractionService else {
result(FlutterError(code: "UNAVAILABLE", message: "语音交互服务实例不可用", details: nil))
return
}
switch call.method {
case "startService":
guard let args = call.arguments as? [String: Any],
let azureKey = args["azure_speech_key"] as? String,
let azureRegion = args["azure_speech_region"] as? String,
let volcanoKey = args["volcano_ai_api_key"] as? String else {
result(FlutterError(code: "INVALID_ARGS", message: "启动服务参数无效", details: nil))
return
}
let success = voiceInteractionService.start(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey)
result(success)
case "stopService":
voiceInteractionService.stop()
result(true)
case "isServiceRunning":
let isRunning = voiceInteractionService.isServiceRunning()
result(isRunning)
case "pauseVoiceInteraction":
voiceInteractionService.pauseVoiceInteraction()
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
// 注册语音交互流通道
let voiceInteractionStreamChannel = FlutterEventChannel(
name: "com.deep_voice.voice_interaction_events",
binaryMessenger: controller.binaryMessenger
)
// 创建语音交互流处理器(作为局部变量)
let voiceInteractionStreamHandler = VoiceInteractionStreamHandler(voiceInteractionService: voiceInteractionService!)
voiceInteractionStreamChannel.setStreamHandler(voiceInteractionStreamHandler)
}
}
// 蓝牙事件流处理器
class BluetoothStreamHandler: NSObject, FlutterStreamHandler, BluetoothEventHandler {
private let bluetoothHelper: ClassicBluetoothHelper
private var eventSink: FlutterEventSink?
init(bluetoothHelper: ClassicBluetoothHelper) {
self.bluetoothHelper = bluetoothHelper
super.init()
// 设置事件处理器
bluetoothHelper.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
// NSLog("BluetoothStreamHandler: 开始监听蓝牙事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
// NSLog("BluetoothStreamHandler: 取消监听蓝牙事件")
eventSink = nil
return nil
}
// 实现BluetoothEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("BluetoothStreamHandler: 接收到蓝牙事件: \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// 媒体按钮事件流处理器
@available(iOS 13.0, *)
class MediaButtonStreamHandler: NSObject, FlutterStreamHandler, MediaButtonEventHandler {
private let mediaButtonHelper: BluetoothMediaButtonHelper
private var eventSink: FlutterEventSink?
init(mediaButtonHelper: BluetoothMediaButtonHelper) {
self.mediaButtonHelper = mediaButtonHelper
super.init()
// 设置事件处理器
mediaButtonHelper.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("MediaButtonStreamHandler: 开始监听媒体按钮事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("MediaButtonStreamHandler: 取消监听媒体按钮事件")
eventSink = nil
return nil
}
// 实现MediaButtonEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("MediaButtonStreamHandler: 收到按钮事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// ASR事件处理器协议
@available(iOS 13.0, *)
protocol AsrEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// ASR事件流处理器
@available(iOS 13.0, *)
class AsrStreamHandler: NSObject, FlutterStreamHandler, AsrEventHandler {
private let azureAsrHelper: AzureAsrHelper
private var eventSink: FlutterEventSink?
init(azureAsrHelper: AzureAsrHelper) {
self.azureAsrHelper = azureAsrHelper
super.init()
// 设置事件处理器
azureAsrHelper.setEventHandler { [weak self] event in
self?.sendEvent(event)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("AsrStreamHandler: 开始监听ASR事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("AsrStreamHandler: 取消监听ASR事件")
eventSink = nil
return nil
}
// 实现AsrEventHandler协议
func sendEvent(_ event: [String: Any]) {
// NSLog("AsrStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// TTS事件处理器协议
@available(iOS 13.0, *)
protocol TtsEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// TTS事件流处理器
@available(iOS 13.0, *)
class TtsStreamHandler: NSObject, FlutterStreamHandler, TtsEventHandler {
private let azureTtsHelper: AzureTtsHelper
private var eventSink: FlutterEventSink?
init(azureTtsHelper: AzureTtsHelper) {
self.azureTtsHelper = azureTtsHelper
super.init()
// 设置事件处理器
azureTtsHelper.setEventHandler { [weak self] event in
self?.sendEvent(event)
}
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("TtsStreamHandler: 开始监听TTS事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("TtsStreamHandler: 取消监听TTS事件")
eventSink = nil
return nil
}
// 实现TtsEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("TtsStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}
// 火山AI事件流处理器
@available(iOS 13.0, *)
class VolcanoAIStreamHandler: NSObject, FlutterStreamHandler {
private let volcanoAIService: VolcanoAIService
private var eventSink: FlutterEventSink?
init(volcanoAIService: VolcanoAIService) {
self.volcanoAIService = volcanoAIService
super.init()
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}
// 处理来自Flutter的流式请求
func handleStreamRequest(messages: [[String: Any]], systemPrompt: String) {
volcanoAIService.sendMessageStream(messages: messages, systemPrompt: systemPrompt, streamCallback: VolcanoAIService.StreamCallback(
onToken: { [weak self] token in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "token",
"token": token
]
DispatchQueue.main.async {
eventSink(event)
}
},
onComplete: { [weak self] in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "complete"
]
DispatchQueue.main.async {
eventSink(event)
}
},
onError: { [weak self] error in
guard let self = self, let eventSink = self.eventSink else { return }
let event: [String: Any] = [
"type": "error",
"error": error.localizedDescription
]
DispatchQueue.main.async {
eventSink(event)
}
}
))
}
}
// 语音交互事件处理器协议
@available(iOS 13.0, *)
protocol VoiceInteractionEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
// 语音交互流处理器
@available(iOS 13.0, *)
class VoiceInteractionStreamHandler: NSObject, FlutterStreamHandler, VoiceInteractionEventHandler {
private let voiceInteractionService: VoiceInteractionService
private var eventSink: FlutterEventSink?
init(voiceInteractionService: VoiceInteractionService) {
self.voiceInteractionService = voiceInteractionService
super.init()
// 设置事件处理器
voiceInteractionService.setEventHandler(self)
}
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
NSLog("VoiceInteractionStreamHandler: 开始监听语音交互事件")
self.eventSink = events
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
NSLog("VoiceInteractionStreamHandler: 取消监听语音交互事件")
eventSink = nil
return nil
}
// 实现VoiceInteractionEventHandler协议
func sendEvent(_ event: [String: Any]) {
NSLog("VoiceInteractionStreamHandler: 收到事件 \(event)")
DispatchQueue.main.async { [weak self] in
self?.eventSink?(event)
}
}
}

589
ios/Runner/AudioSessionManager.swift

@ -1,589 +0,0 @@
import Foundation
import AVFoundation
import UIKit
/// 统一音频会话管理类
/// 用于统一管理所有组件的音频会话配置
@objc class AudioSessionManager: NSObject {
// MARK: - 单例实现
@objc static let shared = AudioSessionManager()
// 音频会话
private let audioSession = AVAudioSession.sharedInstance()
// 音频会话状态
private var isConfigured = false
private var isActive = false
// 当前配置类型
private var currentCategory: AVAudioSession.Category?
private var currentMode: AVAudioSession.Mode?
private var currentOptions: AVAudioSession.CategoryOptions?
// 中断监听器
private var interruptionObserver: NSObjectProtocol?
// 路由变化监听器
private var routeChangeObserver: NSObjectProtocol?
// 私有初始化方法,确保只能通过shared访问
private override init() {
super.init()
NSLog("[AudioSessionManager] 音频会话管理器已初始化")
setupNotifications()
}
deinit {
removeNotifications()
}
// MARK: - 通知设置
private func setupNotifications() {
// 监听中断事件
interruptionObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.handleInterruption(notification)
}
// 监听路由变化事件
routeChangeObserver = NotificationCenter.default.addObserver(
forName: AVAudioSession.routeChangeNotification,
object: nil,
queue: .main) { [weak self] notification in
self?.handleRouteChange(notification)
}
// 监听应用进入后台/前台
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAppWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
}
private func removeNotifications() {
if let interruptionObserver = interruptionObserver {
NotificationCenter.default.removeObserver(interruptionObserver)
}
if let routeChangeObserver = routeChangeObserver {
NotificationCenter.default.removeObserver(routeChangeObserver)
}
NotificationCenter.default.removeObserver(self)
}
// MARK: - 通知处理
private func handleInterruption(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
return
}
switch type {
case .began:
// 中断开始
NSLog("[AudioSessionManager] 音频会话被中断")
isActive = false
case .ended:
// 中断结束,尝试恢复
if let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt {
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
NSLog("[AudioSessionManager] 尝试恢复被中断的音频会话")
try? activateSession()
}
}
@unknown default:
break
}
}
private func handleRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable:
// 新设备连接
let currentRoute = audioSession.currentRoute
for output in currentRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP {
NSLog("[AudioSessionManager] 检测到蓝牙音频设备连接: \(output.portName)")
// 蓝牙设备连接,可能需要更新配置
if isActive && currentOptions?.contains(.allowBluetooth) == true {
try? forceReconfigure()
}
break
}
case .oldDeviceUnavailable:
// 设备断开连接
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == .bluetoothA2DP || output.portType == .bluetoothHFP {
NSLog("[AudioSessionManager] 蓝牙音频设备断开连接: \(output.portName)")
// 蓝牙设备断开,重新配置到内置扬声器
if isActive {
try? forceReconfigure()
}
break
}
}
default:
break
}
}
@objc private func handleAppDidEnterBackground() {
NSLog("[AudioSessionManager] 应用进入后台")
// 应用进入后台时可能需要调整音频会话
}
@objc private func handleAppWillEnterForeground() {
NSLog("[AudioSessionManager] 应用即将进入前台")
// 应用返回前台,检查并恢复音频会话
if isConfigured && !isActive {
try? activateSession()
}
}
// MARK: - 音频会话配置
/// 统一配置方法 - 所有音频配置都通过此方法进行
@objc func configureSession(category: AVAudioSession.Category,
mode: AVAudioSession.Mode,
options: AVAudioSession.CategoryOptions,
force: Bool = false) throws {
NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode), 选项=\(options), 强制=\(force)")
// 如果已配置且不强制重新配置,则直接返回
if isConfigured && !force &&
currentCategory == category &&
currentMode == mode &&
currentOptions == options {
NSLog("[AudioSessionManager] 音频会话已配置,跳过配置")
return
}
do {
// NSLog("[AudioSessionManager] 配置音频会话: 类别=\(category), 模式=\(mode)")
// 暂时停用当前会话(如果正在活动)
if isActive {
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
}
// 设置新的分类、模式和选项
try audioSession.setCategory(category, mode: mode, options: options)
// 设置采样率和缓冲区大小(对于语音识别优化)
// if mode == .spokenAudio {
// try audioSession.setPreferredSampleRate(16000)
// try audioSession.setPreferredIOBufferDuration(0.01) // 10ms
// }
// 重新激活会话
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
isActive = true
// 切换到扬声器
// 记录当前配置
currentCategory = category
currentMode = mode
currentOptions = options
isConfigured = true
// 检查并设置首选的输入设备(如果需要)
if category == .playAndRecord || category == .record {
try configurePreferredInput()
}
return
} catch {
NSLog("[AudioSessionManager] 配置音频会话失败: \(error.localizedDescription)")
throw error
}
}
/// 配置首选输入设备
private func configurePreferredInput() throws {
// 获取可用输入设备
guard let availableInputs = audioSession.availableInputs else {
NSLog("[AudioSessionManager] 无法获取可用输入设备")
return
}
// 首选顺序:有线耳机 > 蓝牙耳机 > 内置麦克风
var preferredInput: AVAudioSessionPortDescription? = nil
// 先尝试找到有线耳机
if let wiredHeadset = availableInputs.first(where: { $0.portType == .headsetMic }) {
preferredInput = wiredHeadset
NSLog("[AudioSessionManager] 设置首选输入设备: 有线耳机")
}
// 如果没有有线耳机,尝试使用蓝牙
else if let bluetooth = availableInputs.first(where: { $0.portType == .bluetoothHFP }) {
preferredInput = bluetooth
NSLog("[AudioSessionManager] 设置首选输入设备: 蓝牙耳机")
}
// 如果没有外部设备,使用内置麦克风
else if let builtIn = availableInputs.first(where: { $0.portType == .builtInMic }) {
preferredInput = builtIn
NSLog("[AudioSessionManager] 设置首选输入设备: 内置麦克风")
}
// 设置首选输入设备
if let input = preferredInput {
try audioSession.setPreferredInput(input)
}
}
/// 激活音频会话(如果已配置)
@objc func activateSession() throws {
if !isConfigured {
NSLog("[AudioSessionManager] 警告: 尝试激活未配置的音频会话,应用默认配置")
// 未配置,应用默认配置
// try configureSession(
// category: .playAndRecord,
// mode: .spokenAudio,
// options: [.allowBluetooth, .allowBluetoothA2DP, .duckOthers, .defaultToSpeaker],
// force: true
// )
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: false // 是否强制重新配置
)
} else if !isActive {
NSLog("[AudioSessionManager] 激活已配置的音频会话")
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
isActive = true
}
}
/// 停用音频会话(保留配置)
@objc func deactivateSession() throws {
if isActive {
NSLog("[AudioSessionManager] 停用音频会话")
try audioSession.setActive(false, options: .notifyOthersOnDeactivation)
isActive = false
}
}
/// 重新配置当前会话
@objc func forceReconfigure() throws {
NSLog("[AudioSessionManager] 强制重新配置音频会话")
// try configureSession(
// category: currentCategory ?? .playAndRecord,
// mode: currentMode ?? .default,
// options: currentOptions ?? [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker],
// force: true
// )
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: false // 是否强制重新配置
)
try audioSession.overrideOutputAudioPort(.speaker)
}
// MARK: - 状态检查
/// 检查麦克风权限
@objc func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
switch AVAudioSession.sharedInstance().recordPermission {
case .granted:
completion(true)
case .denied:
NSLog("[AudioSessionManager] 麦克风权限被拒绝")
completion(false)
case .undetermined:
NSLog("[AudioSessionManager] 请求麦克风权限")
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async {
completion(granted)
}
}
@unknown default:
completion(false)
}
}
/// 获取当前音频会话的配置信息
@objc func getCurrentConfiguration() -> [String: Any] {
return [
"isConfigured": isConfigured,
"isActive": isActive,
"category": String(describing: currentCategory),
"mode": String(describing: currentMode),
"bluetoothEnabled": currentOptions?.contains(.allowBluetooth) == true,
"currentInputs": audioSession.currentRoute.inputs.map { [$0.portType.rawValue: $0.portName] },
"currentOutputs": audioSession.currentRoute.outputs.map { [$0.portType.rawValue: $0.portName] }
]
}
/// 检查当前是否有蓝牙耳机连接
@objc func hasBluetoothHeadsetConnected() -> Bool {
let outputs = audioSession.currentRoute.outputs
return outputs.contains { $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE }
}
/// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
// 在iOS中无法直接检查蓝牙是否启用,我们可以通过检查音频会话配置来推断
// 检查音频会话是否配置了允许蓝牙
if let options = currentOptions {
return options.contains(.allowBluetooth) || options.contains(.allowBluetoothA2DP)
}
// 如果尚未配置音频会话,尝试检查当前路由中是否有蓝牙设备
let outputs = audioSession.currentRoute.outputs
let hasBluetooth = outputs.contains {
$0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP || $0.portType == .bluetoothLE
}
return hasBluetooth
}
/// 获取当前音频路由
@objc func getCurrentAudioRoute() -> String {
let outputs = audioSession.currentRoute.outputs
if let output = outputs.first {
return "\(output.portType.rawValue) (\(output.portName))"
}
return "unknown"
}
/// 获取当前输入设备
@objc func getCurrentInputDevice() -> String {
let inputs = audioSession.currentRoute.inputs
if let input = inputs.first {
return "\(input.portType.rawValue) (\(input.portName))"
}
return "unknown"
}
// MARK: - 路由管理和监听
/// 获取当前路由信息
@objc func getCurrentRoute() -> AVAudioSessionRouteDescription {
return audioSession.currentRoute
}
/// 添加路由变更监听器
/// - Parameters:
/// - observer: 监听对象
/// - selector: 监听方法选择器
@objc func addRouteChangeListener(_ observer: Any, selector: Selector) {
NotificationCenter.default.addObserver(
observer,
selector: selector,
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
/// 移除路由变更监听器
/// - Parameter observer: 要移除的监听对象
@objc func removeRouteChangeListener(_ observer: Any) {
NotificationCenter.default.removeObserver(
observer,
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
/// 获取设备类型描述
private func getDeviceType(portType: AVAudioSession.Port) -> String {
switch portType {
case .bluetoothA2DP, .bluetoothHFP, .bluetoothLE:
return "bluetooth"
case .headphones, .headsetMic:
return "wired"
case .airPlay:
return "airplay"
default:
return "unknown"
}
}
// /// 为Azure语音识别专门配置的音频会话
// /// 此函数优化Azure语音识别的音频处理配置,提供更强的回音消除能力
// /// - Parameters:
// /// - force: 是否强制重新配置
// /// - configureAdditionalSettings: 配置完成后进行额外的音频设置
// /// - Returns: 配置是否成功
// @objc func configureForAzureSpeechRecognition(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool {
// NSLog("[AudioSessionManager] 配置Azure ASR语音识别的音频会话")
// do {
// // 配置音频会话以优化语音识别
// try configureSession(
// category: .playAndRecord, // 允许同时录音和播放
// mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
// options: [
// .allowBluetooth, // 允许蓝牙设备
// .defaultToSpeaker, // 默认使用扬声器
// .mixWithOthers // 允许与其他应用混音
// ],
// force: force // 是否强制重新配置
// )
// // 额外的优化配置
// if configureAdditionalSettings {
// // 获取当前是否使用耳机
// let currentRoute = audioSession.currentRoute
// let hasHeadphones = currentRoute.outputs.contains {
// $0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
// }
// // 设置采样率为16kHz(Azure Speech API推荐)
// try audioSession.setPreferredSampleRate(16000.0)
// // 设置较小的缓冲区大小以减少延迟
// try audioSession.setPreferredIOBufferDuration(0.01)
// // 根据是否有耳机连接调整输入增益
// if !hasHeadphones {
// // 无耳机时降低输入增益以减少回音
// try audioSession.setInputGain(0.8)
// NSLog("[AudioSessionManager] 启用扬声器回音消除优化")
// } else {
// // 使用耳机时可以使用较高增益
// try audioSession.setInputGain(1.0)
// NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式")
// }
// }
// NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话")
// return true
// } catch {
// NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)")
// return false
// }
// }
@objc func configureForVoiceInteraction(force: Bool = false, configureAdditionalSettings: Bool = true) -> Bool {
NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话")
do {
// 配置音频会话以优化语音识别
try configureSession(
category: .playAndRecord, // 允许同时录音和播放
mode: .voiceChat, // 使用voiceChat模式获得最佳回音消除效果
options: [
.allowBluetooth, // 允许蓝牙设备
.defaultToSpeaker, // 默认使用扬声器
.mixWithOthers // 允许与其他应用混音
],
force: force // 是否强制重新配置
)
try audioSession.overrideOutputAudioPort(.speaker)
// 额外的优化配置
if configureAdditionalSettings {
// 获取当前是否使用耳机
let currentRoute = audioSession.currentRoute
let hasHeadphones = currentRoute.outputs.contains {
$0.portType == .headphones || $0.portType == .bluetoothA2DP || $0.portType == .bluetoothHFP
}
// 设置采样率为16kHz(Azure Speech API推荐)
try audioSession.setPreferredSampleRate(16000.0)
// 设置较小的缓冲区大小以减少延迟
try audioSession.setPreferredIOBufferDuration(0.01)
// 根据是否有耳机连接调整输入增益
if !hasHeadphones {
// 无耳机时降低输入增益以减少回音
try audioSession.setInputGain(0.8)
NSLog("[AudioSessionManager] 启用扬声器回音消除优化")
} else {
// 使用耳机时可以使用较高增益
try audioSession.setInputGain(1.0)
NSLog("[AudioSessionManager] 检测到耳机连接,应用耳机模式")
}
}
NSLog("[AudioSessionManager] 已成功配置Azure语音识别的音频会话")
return true
} catch {
NSLog("[AudioSessionManager] 配置Azure语音识别的音频会话失败: \(error.localizedDescription)")
return false
}
}
/// 配置用于语音交互(同时支持ASR和TTS)的音频会话
/// 此函数综合优化语音识别和语音合成,适用于需要双向交互的场景
/// - Returns: 配置是否成功
// @objc func configureForVoiceInteraction(force: Bool = false) -> Bool {
// do {
// // 配置音频会话以支持语音交互
// // 使用playAndRecord类别允许同时录音和播放
// // 使用spokenAudio模式优化语音交互
// try configureSession(
// category: .playAndRecord,
// mode: .voiceChat,
// options: [
// // .allowBluetoothA2DP, // 允许通过蓝牙A2DP连接进行高质量音频输出
// .allowBluetooth, // 允许通过蓝牙SCO连接进行输入和输出
// .defaultToSpeaker, // 默认使用扬声器输出
// .mixWithOthers
// ],
// force: force // 强制重新配置,确保设置生效
// )
// // 设置首选输入设备
// try configurePreferredInput()
// NSLog("[AudioSessionManager] 已配置语音交互(ASR+TTS)的音频会话")
// return true
// } catch {
// NSLog("[AudioSessionManager] 配置语音交互(ASR+TTS)的音频会话失败: \(error.localizedDescription)")
// return false
// }
// }
}

813
ios/Runner/AzureAsrHelper.swift

@ -1,813 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
import AudioToolbox
/// Azure ASR工具类,负责实现语音识别服务接口
@available(iOS 13.0, *)
class AzureAsrHelper: NSObject {
// MARK: - 属性
/// 是否使用自定义音频处理器 - 设置为false时将使用系统默认麦克风输入
private let useCustomAudioProcessor = true
/// 事件处理回调
private var eventHandler: (([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
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// MARK: - 初始化
override init() {
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?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 检查语言参数
if let languages = supportedLanguages, languages.isEmpty {
print("[AzureAsrHelper] 警告: 传入的支持语言列表为空,将使用默认语言")
}
// 释放之前的资源
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()
// 根据设置决定是否使用自定义音频处理器
if useCustomAudioProcessor {
// 创建自定义音频流和处理器,替代默认的麦克风输入
pushStream = try SPXPushAudioInputStream()
audioConfig = try SPXAudioConfiguration(streamInput: pushStream!)
// 初始化自定义音频处理器
audioProcessor = CustomAudioProcessor()
} else {
// 使用默认麦克风输入
audioConfig = try SPXAudioConfiguration()
}
// 设置语言配置
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?(["type": "error", "message": "创建识别器失败: \(error.localizedDescription)"])
return false
}
}
/// 设置音频会话
private func setupAudioSession() throws {
print("[AzureAsrHelper] 开始配置音频会话...")
// 使用AudioSessionManager配置音频会话,使用专门为Azure ASR优化的配置
let success = audioSessionManager.configureForVoiceInteraction()
if !success {
print("[AzureAsrHelper] 警告: 通过AudioSessionManager配置音频会话失败")
}
}
/// 设置所有回调
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?(["type": "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?(["type": "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?(["type": "sessionStarted"])
}
recognizer.addSessionStoppedEventHandler { [weak self] _, _ in
guard let self = self else { return }
print("[AzureAsrHelper] 识别会话已结束")
self._isContinuousRecognitionActive = false
self.eventHandler?(["type": "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?(["type": "canceled", "reason": reason, "errorDetails": errorDetails])
self._isContinuousRecognitionActive = false
}
}
/// 执行一次性语音识别
/// - Returns: 是否成功启动识别
func recognizeOnce() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 如果正在连续识别,先停止
if _isContinuousRecognitionActive {
stopContinuousRecognition()
}
// 确保识别器已创建
if recognizer == nil && !createRecognizerAndSetupCallbacks() {
return false
}
do {
// 启动音频处理
startAudioProcessing()
// 通知会话开始
eventHandler?(["type": "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?(["type": "result",
"text": result.text ?? "",
"detectedLanguage": detectedLanguage
])
} else if result.reason == SPXResultReason.noMatch {
print("[AzureAsrHelper] 无匹配结果")
self.eventHandler?(["type": "noMatch"])
} else if result.reason == SPXResultReason.canceled {
do {
let details = try SPXCancellationDetails(fromCanceledRecognitionResult: result)
let errorDetails = details.errorDetails ?? "未知错误"
self.eventHandler?(["type": "error", "message": "识别取消: \(errorDetails)"])
} catch {
print("[AzureAsrHelper] 错误: 获取取消详情失败: \(error.localizedDescription)")
self.eventHandler?(["type": "error", "message": "识别取消,无法获取详细原因"])
}
}
}
return true
} catch {
print("[AzureAsrHelper] 错误: 识别异常: \(error.localizedDescription)")
eventHandler?(["type": "error", "message": "识别异常: \(error.localizedDescription)"])
stopAudioProcessing()
return false
}
}
/// 开始连续语音识别
/// - Returns: 是否成功启动识别
func startContinuousRecognition() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureAsrHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
// 检查是否已经在识别
if _isContinuousRecognitionActive {
print("[AzureAsrHelper] 已经在进行连续识别中,忽略请求")
return true
}
// 确保识别器已创建
if recognizer == nil {
print("[AzureAsrHelper] 尝试重新创建识别器...")
if !createRecognizerAndSetupCallbacks() {
eventHandler?(["type": "error", "message": "无法创建语音识别器"])
return false
}
}
// 启动音频处理
startAudioProcessing()
// 尝试启动连续识别
do {
// 启动连续识别
try recognizer?.startContinuousRecognition()
_isContinuousRecognitionActive = true
print("[AzureAsrHelper] 连续识别已启动")
return true
} catch {
_isContinuousRecognitionActive = false
print("[AzureAsrHelper] 错误: 开始连续识别失败: \(error.localizedDescription)")
// 停止音频处理
stopAudioProcessing()
// 发送错误通知
eventHandler?(["type": "error", "message": "开始连续识别失败: \(error.localizedDescription)"])
return false
}
}
/// 停止连续语音识别
/// - Returns: 是否成功停止识别
func stopContinuousRecognition() -> Bool {
// 停止音频处理
stopAudioProcessing()
// 检查是否初始化
if !isInitialized {
print("[AzureAsrHelper] 错误: 语音服务未初始化")
eventHandler?(["type": "error", "message": "语音服务未初始化"])
return false
}
if !_isContinuousRecognitionActive {
print("[AzureAsrHelper] 未进行连续识别,忽略停止请求")
return true
}
print("[AzureAsrHelper] 停止连续语音识别")
// 防止空指针异常
guard let recognizer = recognizer else {
print("[AzureAsrHelper] 警告: 识别器为空,但状态显示活跃")
_isContinuousRecognitionActive = false
eventHandler?(["type": "sessionStopped"])
return true
}
// 先标记为非活跃状态,防止重复调用
_isContinuousRecognitionActive = false
// 异步执行停止操作,避免阻塞主线程
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {
// 在后台线程执行停止操作
try recognizer.stopContinuousRecognition()
// 主线程回调通知结果
DispatchQueue.main.async {
print("[AzureAsrHelper] 连续识别已停止")
self.eventHandler?(["type": "sessionStopped"])
}
} catch {
// 主线程回调通知错误
DispatchQueue.main.async {
print("[AzureAsrHelper] 错误: 停止连续识别失败: \(error.localizedDescription)")
// 停止失败,恢复状态
self._isContinuousRecognitionActive = true
self.eventHandler?(["type": "error", "message": "停止识别失败: \(error.localizedDescription)"])
}
}
}
// 立即返回,不阻塞调用方
return true
}
/// 检查连续识别是否活跃
/// - Returns: 连续识别是否处于活跃状态
func isContinuousRecognitionActive() -> Bool {
return _isContinuousRecognitionActive
}
/// 释放资源
func dispose() {
// 停止音频处理
stopAudioProcessing()
// 尝试停止所有识别操作
if _isContinuousRecognitionActive {
do {
if let recognizer = recognizer {
try recognizer.stopContinuousRecognition()
print("[AzureAsrHelper] 连续识别已停止")
}
} catch {
print("[AzureAsrHelper] 警告: 停止连续识别失败: \(error.localizedDescription)")
}
_isContinuousRecognitionActive = false
}
// 释放识别器
recognizer = nil
audioConfig = nil
speechConfig = nil
pushStream = nil
audioProcessor = nil
// 重置状态
isInitialized = false
print("[AzureAsrHelper] 资源已释放")
}
/// 从结果中获取检测到的语言
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
}
}
/// 设置事件处理器
@objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
// MARK: - 音频处理
/// 开始音频处理
private func startAudioProcessing() {
// 如果未使用自定义音频处理器,跳过
if !useCustomAudioProcessor {
return
}
guard !isProcessingAudio, let audioProcessor = audioProcessor else { return }
isProcessingAudio = true
// 启动音频处理器
if !audioProcessor.startRecord() {
print("[AzureAsrHelper] 错误: 启动音频处理器失败")
eventHandler?(["type": "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?(["type": "audioData", "data": bytes])
}
}
print("[AzureAsrHelper] 音频处理已启动")
}
/// 停止音频处理
private func stopAudioProcessing() {
// 如果未使用自定义音频处理器,跳过
if !useCustomAudioProcessor {
return
}
// 停止定时器
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
}
// 启用回音消除 - 注意: kAUVoiceIOProperty_BypassVoiceProcessing值为1时表示绕过处理,值为0表示启用处理
if isEchoCancellationEnabled {
var echoCancellation: UInt32 = 0 // 0表示不绕过,即启用回音消除
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
}
// 保存处理后的数据
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
}
}

454
ios/Runner/AzureTtsHelper.swift

@ -1,454 +0,0 @@
import Foundation
import MicrosoftCognitiveServicesSpeech
import AVFoundation
/// Azure TTS工具类,负责实现TTS服务接口
@available(iOS 13.0, *)
class AzureTtsHelper: NSObject {
// MARK: - 属性
/// 事件处理回调
private var eventHandler: (([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
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// 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: - 初始化
override init() {
super.init()
}
deinit {
dispose()
}
/// 设置事件处理器
@objc func setEventHandler(_ handler: @escaping ([String: Any]) -> Void) {
self.eventHandler = handler
}
// 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?(["type": "error", "message": "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?(["type": "error", "message": "初始化语音合成服务失败: \(error.localizedDescription)"])
return false
}
}
/// 配置音频会话
private func configureAudioSession() -> Bool {
do {
// 使用音频会话管理器配置媒体播放模式
try audioSessionManager.configureForVoiceInteraction()
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?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
return false
}
if voiceName.isEmpty {
print("[AzureTtsHelper] 错误: 声音名称为空")
eventHandler?(["type": "error", "message": "声音名称不能为空"])
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?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
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?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
// 检查参数有效性
if speechSubscriptionKey.isEmpty || serviceRegion.isEmpty {
print("[AzureTtsHelper] 错误: Azure 配置信息不完整")
eventHandler?(["type": "error", "message": "Azure 配置信息不完整"])
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?(["type": "error", "message": "合成器未初始化"])
return false
}
_isSpeaking = true
eventHandler?(["type": "started"])
Task {
do {
// 使用异步方法进行合成并直接播放
_ = try synthesizer.startSpeakingSsml(text)
} catch {
print("[AzureTtsHelper] 错误: 语音合成失败: \(error.localizedDescription)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler?(["type": "error", "message": "语音合成失败: \(error.localizedDescription)"])
}
}
}
return true
}
/// 停止当前语音合成
/// - Returns: 操作是否成功
func stopSpeaking() -> Bool {
// 检查是否初始化
if !isInitialized {
print("[AzureTtsHelper] 错误: TTS 引擎尚未初始化")
eventHandler?(["type": "error", "message": "TTS 引擎尚未初始化"])
return false
}
if !_isSpeaking {
print("[AzureTtsHelper] 未进行语音播放,忽略停止请求")
return true
}
// 停止合成
do {
try synthesizer?.stopSpeaking()
_isSpeaking = false
eventHandler?(["type": "canceled"])
print("[AzureTtsHelper] 已停止语音合成")
return true
} catch {
print("[AzureTtsHelper] 错误: 停止语音合成失败: \(error.localizedDescription)")
eventHandler?(["type": "error", "message": "停止语音合成失败: \(error.localizedDescription)"])
return false
}
}
/// 检查是否正在播放
/// - Returns: 当前是否正在播放语音
func isSpeaking() -> Bool {
return _isSpeaking
}
/// 释放资源
func dispose() {
// 尝试停止所有语音合成
if _isSpeaking {
do {
if let synthesizer = synthesizer {
try synthesizer.stopSpeaking()
print("[AzureTtsHelper] 语音合成已停止")
}
} catch {
print("[AzureTtsHelper] 警告: 停止语音合成失败: \(error.localizedDescription)")
}
_isSpeaking = false
}
// 释放合成器
synthesizer = nil
speechConfig = nil
// 重置状态
isInitialized = false
isAudioSessionConfigured = false
print("[AzureTtsHelper] 资源已释放")
}
// 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?(["type": "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?(["type": "error", "message": "语音合成取消: \(cancellationDetails.errorDetails ?? "未知错误")"])
}
} catch {
print("[AzureTtsHelper] 获取取消详情时出错: \(error)")
DispatchQueue.main.async {
self._isSpeaking = false
self.eventHandler?(["type": "error", "message": "语音合成被取消"])
}
}
}
// 合成开始事件
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
}
}

13
ios/Runner/Base.lproj/Main.storyboard

@ -1,10 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
@ -16,14 +14,13 @@
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-26" y="-77"/>
</scene>
</scenes>
</document>

464
ios/Runner/BluetoothMediaButtonHelper.swift

@ -1,464 +0,0 @@
import Foundation
import MediaPlayer
import AVFoundation
import UIKit
/// 媒体按钮事件处理协议
@available(iOS 13.0, *)
@objc protocol MediaButtonEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
/// 蓝牙媒体按键事件处理类
/// 该类仅在iOS 13.0及以上版本可用
@available(iOS 13.0, *)
class BluetoothMediaButtonHelper: NSObject, AVAudioPlayerDelegate {
// MARK: - 属性
@objc static let shared = BluetoothMediaButtonHelper()
/// 日志标签
private let TAG = "BluetoothMediaButtonHelper"
/// 远程控制中心
private var remoteCommandCenter: MPRemoteCommandCenter
/// 事件处理器 - 用于发送事件到Flutter
private weak var eventHandler: MediaButtonEventHandler?
/// 是否已启用监听
private var isListening = false
/// 静音音频播放器 - 用于保持音频会话活跃
private var silentAudioPlayer: AVAudioPlayer?
/// 播放计时器 - 用于定期更新播放信息
private var playbackTimer: Timer?
/// 当前播放时间 - 模拟播放进度
private var currentPlaybackTime: TimeInterval = 0
/// 总时长 - 模拟
private let totalDuration: TimeInterval = 300.0
/// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// MARK: - 初始化
override init() {
// 获取远程控制中心单例
self.remoteCommandCenter = MPRemoteCommandCenter.shared()
super.init()
NSLog("\(TAG): 初始化媒体按钮处理器")
// 初始化静音音频播放器
initSilentAudioPlayer()
}
deinit {
stopButtonListening()
NSLog("\(TAG): 媒体按钮处理器已释放")
}
// MARK: - 公共方法
/// 设置事件处理器
@objc func setEventHandler(_ handler: MediaButtonEventHandler) {
eventHandler = handler
NSLog("\(TAG): 已设置媒体按钮事件处理器")
}
/// 开始监听蓝牙媒体按钮事件
@objc func startButtonListening() -> Bool {
if isListening {
NSLog("\(TAG): 已经在监听蓝牙媒体按钮事件")
return true
}
NSLog("\(TAG): 开始监听蓝牙媒体按钮事件")
// 立即注册远程命令 - 这不需要音频会话
registerRemoteCommands()
// 标记为已在监听
isListening = true
// 确保NowPlaying信息可用 - 这也不依赖于活跃的音频会话
setupNowPlaying(isPlaying: true)
// 尝试启用静音音频播放(但即使失败,远程命令仍然可用)
do {
// try audioSessionManager.configureForBluetoothMediaButtons()
startSilentAudio()
// 延时1秒
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.setupNowPlaying(isPlaying: false)
self.stopSilentAudio()
}
// startUpdatingPlaybackInfo()
} catch {
NSLog("\(TAG): 音频设置或播放失败: \(error.localizedDescription)")
// 继续执行,因为远程命令在某些设备上不需要播放音频也能工作
}
NSLog("\(TAG): 蓝牙媒体按钮监听已启动")
return true
}
/// 停止监听蓝牙媒体按钮事件
@objc func stopButtonListening() -> Bool {
if !isListening {
return true
}
NSLog("\(TAG): 停止监听蓝牙媒体按钮事件")
// 停止播放静音音频
stopSilentAudio()
// 停止更新播放信息
stopUpdatingPlaybackInfo()
// 取消注册远程命令
unregisterRemoteCommands()
// 清除NowPlaying信息
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
}
isListening = false
NSLog("\(TAG): 蓝牙媒体按钮监听已停止")
return true
}
// MARK: - 私有方法
/// 初始化静音音频播放器
private func initSilentAudioPlayer() {
do {
// 尝试加载sample.mp3文件
if let audioPath = Bundle.main.path(forResource: "sample", ofType: "mp3") {
let audioUrl = URL(fileURLWithPath: audioPath)
silentAudioPlayer = try AVAudioPlayer(contentsOf: audioUrl)
silentAudioPlayer?.numberOfLoops = -1 // 无限循环播放
silentAudioPlayer?.volume = 0.0 // 设置为全音量
silentAudioPlayer?.prepareToPlay() // 预加载
silentAudioPlayer?.delegate = self
NSLog("\(TAG): 音频播放器使用sample.mp3初始化成功")
return
}
NSLog("\(TAG): 未找到sample.mp3文件,使用生成的静音数据")
} catch {
NSLog("\(TAG): 初始化静音音频播放器失败: \(error.localizedDescription)")
}
}
/// 开始播放静音音频
private func startSilentAudio() {
guard let player = silentAudioPlayer else {
NSLog("\(TAG): 无法开始播放,播放器未初始化")
return
}
if !player.isPlaying {
let success = player.play()
if success {
NSLog("\(TAG): 开始播放静音音频")
} else {
NSLog("\(TAG): 播放启动失败")
}
} else {
NSLog("\(TAG): 静音音频已在播放中")
}
}
/// 停止播放静音音频
private func stopSilentAudio() {
silentAudioPlayer?.stop()
NSLog("\(TAG): 停止播放静音音频")
}
/// 开始更新播放信息
private func startUpdatingPlaybackInfo() {
// 初始设置
setupNowPlaying(isPlaying: true)
// 定期更新播放信息 - 恢复定时器,确保持续更新播放状态
playbackTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else { return }
// 更新播放时间(模拟)
self.currentPlaybackTime += 1.0
if self.currentPlaybackTime >= self.totalDuration {
self.currentPlaybackTime = 0.0
}
// 更新NowPlaying信息
self.setupNowPlaying(isPlaying: true)
}
NSLog("\(TAG): 开始更新播放信息")
}
/// 停止更新播放信息
private func stopUpdatingPlaybackInfo() {
playbackTimer?.invalidate()
playbackTimer = nil
// 清空NowPlaying信息
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
}
NSLog("\(TAG): 停止更新播放信息")
}
/// 设置NowPlaying信息
private func setupNowPlaying(isPlaying: Bool) {
NSLog("\(TAG): 设置NowPlaying信息, isPlaying=\(isPlaying)")
// 创建音频专辑图片
let albumArt = UIImage(named: "AppIcon") // 使用应用图标作为专辑图片
var nowPlayingInfo: [String: Any] = [
MPMediaItemPropertyTitle: "DeepSound 语音助手",
MPMediaItemPropertyArtist: "DeepSound",
MPNowPlayingInfoPropertyPlaybackRate: isPlaying ? 1.0 : 0.0, // 1.0表示正在播放
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentPlaybackTime,
MPMediaItemPropertyPlaybackDuration: totalDuration
]
// 如果有专辑图片,添加到NowPlaying信息中
if let albumArt = albumArt {
let artwork = MPMediaItemArtwork(boundsSize: albumArt.size) { size in
return albumArt
}
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
}
// 设置NowPlaying信息 (在主线程执行)
DispatchQueue.main.async {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
NSLog("\(self.TAG): NowPlaying信息设置完成")
}
}
/// 注册远程命令
private func registerRemoteCommands() {
let commandCenter = MPRemoteCommandCenter.shared()
// 播放/暂停命令
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget(self, action: #selector(handlePlayPauseCommand(_:)))
// 下一曲/上一曲命令
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action: #selector(handleNextTrackCommand(_:)))
commandCenter.previousTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.addTarget(self, action: #selector(handlePreviousTrackCommand(_:)))
// 快进/快退命令
commandCenter.seekForwardCommand.isEnabled = true
commandCenter.seekForwardCommand.addTarget(self, action: #selector(handleSeekForwardCommand(_:)))
commandCenter.seekBackwardCommand.isEnabled = true
commandCenter.seekBackwardCommand.addTarget(self, action: #selector(handleSeekBackwardCommand(_:)))
// 停止命令
commandCenter.stopCommand.isEnabled = true
commandCenter.stopCommand.addTarget(self, action: #selector(handleStopCommand(_:)))
NSLog("\(TAG): 远程命令已注册")
}
/// 取消注册远程命令
private func unregisterRemoteCommands() {
NSLog("\(TAG): 取消注册远程命令")
// 基本控制命令
remoteCommandCenter.playCommand.isEnabled = false
remoteCommandCenter.playCommand.removeTarget(nil)
remoteCommandCenter.pauseCommand.isEnabled = false
remoteCommandCenter.pauseCommand.removeTarget(nil)
remoteCommandCenter.togglePlayPauseCommand.isEnabled = false
remoteCommandCenter.togglePlayPauseCommand.removeTarget(nil)
remoteCommandCenter.stopCommand.isEnabled = false
remoteCommandCenter.stopCommand.removeTarget(nil)
}
/// 发送按钮事件到Flutter
private func sendButtonEvent(type: String) {
// 创建事件数据
let eventData: [String: Any] = [
"type": "mediaButtonEvent",
"buttonType": type,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
// 发送事件到处理器
eventHandler?.sendEvent(eventData)
}
// MARK: - 命令处理方法
/// 处理播放/暂停命令
@objc private func handlePlayPauseCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到播放/暂停命令")
// 发送事件到Flutter
sendButtonEvent(type: "play_pause")
// 调用VoiceInteractionService处理媒体按钮事件
if VoiceInteractionService.shared.isServiceRunning() {
NSLog("\(TAG): 转发到VoiceInteractionService处理")
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理下一曲命令
@objc private func handleNextTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到下一曲命令")
// 发送事件到Flutter
sendButtonEvent(type: "next_track")
// 也可以转发到VoiceInteractionService
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理上一曲命令
@objc private func handlePreviousTrackCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到上一曲命令")
// 发送事件到Flutter
sendButtonEvent(type: "previous_track")
// 也可以转发到VoiceInteractionService
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
/// 处理快进命令
@objc private func handleSeekForwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快进命令")
// 发送事件到Flutter
sendButtonEvent(type: "seek_forward")
return .success
}
/// 处理快退命令
@objc private func handleSeekBackwardCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到快退命令")
// 发送事件到Flutter
sendButtonEvent(type: "seek_backward")
return .success
}
/// 处理停止命令
@objc private func handleStopCommand(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus {
NSLog("\(TAG): 收到停止命令")
// 发送事件到Flutter
sendButtonEvent(type: "stop")
// 调用VoiceInteractionService处理媒体按钮事件
if VoiceInteractionService.shared.isServiceRunning() {
VoiceInteractionService.shared.handleMediaButtonAction()
}
return .success
}
// MARK: - AVAudioPlayerDelegate方法
/// 音频播放结束回调
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
NSLog("\(TAG): 音频播放结束, 成功: \(flag)")
// 如果仍在监听状态,重新开始播放
if isListening && player == silentAudioPlayer {
player.play()
}
}
/// 音频播放错误回调
func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
if let error = error {
NSLog("\(TAG): 音频解码错误: \(error.localizedDescription)")
} else {
NSLog("\(TAG): 音频解码发生未知错误")
}
// 重新初始化播放器
if player == silentAudioPlayer && isListening {
initSilentAudioPlayer()
startSilentAudio()
}
}
/// 清理和释放资源
func dispose() {
NSLog("\(TAG): 释放BluetoothMediaButtonHelper资源")
// 停止监听
stopButtonListening()
// 释放播放器和计时器资源
silentAudioPlayer = nil
if let timer = playbackTimer {
timer.invalidate()
playbackTimer = nil
}
// 清除引用
eventHandler = nil
NSLog("\(TAG): 资源已释放")
}
}
extension UInt32 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt32>.size)
}
}
extension UInt16 {
var data: Data {
var int = self
return Data(bytes: &int, count: MemoryLayout<UInt16>.size)
}
}

243
ios/Runner/ClassicBluetoothHelper.swift

@ -1,243 +0,0 @@
import Foundation
import CoreBluetooth
import AVFoundation
import UIKit
/// 蓝牙事件处理协议
@objc protocol BluetoothEventHandler: AnyObject {
func sendEvent(_ event: [String: Any])
}
@objc class ClassicBluetoothHelper: NSObject, CBCentralManagerDelegate {
// MARK: - 属性
// 音频会话管理器
private let audioSessionManager = AudioSessionManager.shared
// 蓝牙管理器
private var centralManager: CBCentralManager?
// 事件处理器
private weak var eventHandler: BluetoothEventHandler?
// MARK: - 初始化与设置
override init() {
super.init()
}
// 设置事件处理器
@objc func setEventHandler(_ handler: BluetoothEventHandler) {
eventHandler = handler
}
// MARK: - CBCentralManagerDelegate
func centralManagerDidUpdateState(_ central: CBCentralManager) {
let stateString = getBluetoothStateString(state: central.state)
sendBluetoothStateEvent(state: stateString)
}
// MARK: - 设备监控
// 获取蓝牙状态字符串
private func getBluetoothStateString(state: CBManagerState) -> String {
switch state {
case .poweredOn:
return "on"
case .poweredOff:
return "off"
default:
return "unknown"
}
}
// 发送蓝牙状态事件
private func sendBluetoothStateEvent(state: String) {
let event: [String: Any] = [
"type": "bluetoothStateChanged",
"state": state,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// 启动蓝牙设备监控
@objc func initialize() {
if centralManager == nil {
centralManager = CBCentralManager(delegate: self, queue: nil)
}
// 发送当前蓝牙状态
if let manager = centralManager {
let stateString = getBluetoothStateString(state: manager.state)
sendBluetoothStateEvent(state: stateString)
}
// 移除之前的监听器
NotificationCenter.default.removeObserver(self)
// 添加新的监听器
audioSessionManager.addRouteChangeListener(self, selector: #selector(handleRouteChange(_:)))
// BluetoothMediaButtonHelper.shared.startButtonListening()
}
// 处理音频路由变化
@objc private func handleRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
// 获取当前设备
let currentDevices = getConnectedAudioDevices()
// 检查原因
switch reason {
case .newDeviceAvailable:
// 新设备已连接
for device in currentDevices {
// 发送设备连接事件
NSLog("发送设备连接事件 - 设备: \(device["name"] ?? "未知设备") [\(device["address"] ?? "未知地址")]")
sendDeviceConnectedEvent(device: device)
}
case .oldDeviceUnavailable:
// 获取断开之前的路由
if let previousRoute = userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
// 过滤蓝牙相关设备
for output in previousRoute.outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid)
]
// 发送设备断开事件
sendDeviceDisconnectedEvent(device: device)
}
}
default:
break
}
}
// 检查是否为蓝牙输出
private func isBluetoothOutput(_ portType: AVAudioSession.Port) -> Bool {
return portType == .bluetoothA2DP || portType == .bluetoothHFP || portType == .bluetoothLE
}
// MARK: - 设备事件
// 发送设备连接事件
private func sendDeviceConnectedEvent(device: [String: String]) {
guard let name = device["name"], let address = device["address"] else { return }
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "headset"
]
let event: [String: Any] = [
"type": "deviceConnected",
"device": deviceMap,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// 发送设备断开事件
private func sendDeviceDisconnectedEvent(device: [String: String]) {
guard let name = device["name"], let address = device["address"] else { return }
let deviceMap: [String: Any] = [
"name": name,
"address": address,
"type": "headset"
]
let event: [String: Any] = [
"type": "deviceDisconnected",
"device": deviceMap,
"timestamp": Int(Date().timeIntervalSince1970 * 1000)
]
eventHandler?.sendEvent(event)
}
// MARK: - 公共API
// 获取当前连接的耳机设备
@objc func getConnectedHeadsetDevices(_ completion: @escaping ([[String: String]]?, String?) -> Void) {
// 检查蓝牙状态
if centralManager?.state != .poweredOn {
completion(nil, "蓝牙未启用")
return
}
do {
// 获取设备
let devices = getConnectedAudioDevices()
completion(devices, nil)
} catch {
completion(nil, "获取设备失败: \(error.localizedDescription)")
}
}
// 获取当前连接的音频设备
private func getConnectedAudioDevices() -> [[String: String]] {
var result: [[String: String]] = []
// 获取当前音频路由
let outputs = audioSessionManager.getCurrentRoute().outputs
// 过滤蓝牙相关设备
for output in outputs where isBluetoothOutput(output.portType) {
let device: [String: String] = [
"name": output.portName,
"address": cleanDeviceAddress(output.uid) // 处理设备地址,移除后缀
]
result.append(device)
}
return result
}
// 释放资源
@objc func dispose() {
audioSessionManager.removeRouteChangeListener(self)
eventHandler = nil
}
// 检查蓝牙是否启用
@objc func isBluetoothEnabled() -> Bool {
return centralManager?.state == .poweredOn
}
// 清理设备地址,移除后缀
private func cleanDeviceAddress(_ address: String) -> String {
// 检查是否包含常见分隔符
if let range = address.range(of: "-") ?? address.range(of: ":") ?? address.range(of: "_") {
// 只保留分隔符之前的部分
return String(address[..<range.lowerBound])
}
// iOS设备UID有时会包含额外信息,尝试只保留标准MAC地址格式部分
// MAC地址通常是12个十六进制字符,可能带有分隔符
let hexPattern = "[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}|[0-9A-Fa-f]{12}"
if let regex = try? NSRegularExpression(pattern: hexPattern, options: []),
let match = regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) {
let matchRange = match.range
if let range = Range(matchRange, in: address) {
return String(address[range])
}
}
return address
}
}

118
ios/Runner/Info.plist

@ -2,46 +2,14 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yunqiinnovation.deepsound</string>
<key>CFBundleURLSchemes</key>
<array>
<string>fb3829022037361855</string>
<string>com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278</string>
</array>
</dict>
</array>
<key>FacebookAppID</key>
<string>3829022037361855</string>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>FacebookDisplayName</key>
<string>Deepsound</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fbapi</string>
<string>fb-messenger-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
<string>googlechrome</string>
<string>googlechromes</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>DeepSound</string>
<string>Deep Voice</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>NSUserTrackingUsageDescription</key>
<string>This application needs to access your tracking data in order to offer better personalized services and advertisements</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
@ -54,23 +22,63 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>fb3829022037361855</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>wx5bbb7be9324f3e2d</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
<string>bluetooth-peripheral</string>
<string>external-accessory</string>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>weixin</string>
<string>weixinULAPI</string>
<string>weixinURLParamsAPI</string>
<string>fbapi</string>
<string>fbapi20130214</string>
<string>fbapi20130410</string>
<string>fbapi20130702</string>
<string>fbapi20131010</string>
<string>fbapi20131219</string>
<string>fbapi20140410</string>
<string>fbapi20140116</string>
<string>fbapi20150313</string>
<string>fbapi20150629</string>
<string>fbapi20160328</string>
<string>fbauth</string>
<string>fb-messenger-share-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth permission is required to connect and manage audio devices</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Bluetooth permission is required to connect and manage audio devices</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone permission is required for voice recognition</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Speech recognition permission is required to provide voice interaction services</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@ -88,5 +96,27 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>需要蓝牙权限用于连接和管理音频设备</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>需要位置权限用于搜索附近的蓝牙设备</string>
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限用于语音识别和录音功能</string>
<key>FacebookClientToken</key>
<string>193d0a2b1757c41cfa557ea8e8f21c33</string>
<key>FacebookAppID</key>
<string>3829022037361855</string>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<true/>
<key>FacebookAutoLogAppEventsEnabled</key>
<true/>
<key>NSAccessorySetupKitSupports</key>
<array>
<string>Bluetooth</string>
</array>
</dict>
</plist>

0
ios/Runner/Resources/.gitkeep

BIN
ios/Runner/Resources/sample.mp3

Binary file not shown.

BIN
ios/Runner/Resources/silence.mp3

Binary file not shown.

6
ios/Runner/Runner-Bridging-Header.h

@ -1,7 +1 @@
#ifndef Runner_Bridging_Header_h
#define Runner_Bridging_Header_h
#import "GeneratedPluginRegistrant.h"
#import <MicrosoftCognitiveServicesSpeech/SPXSpeechApi.h>
#endif /* Runner_Bridging_Header_h */

5
ios/Runner/Runner.entitlements

@ -0,0 +1,5 @@
<?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/>
</plist>

565
ios/Runner/VoiceInteractionService.swift

@ -1,565 +0,0 @@
import Foundation
import UIKit
import AVFoundation
/**
* 语音交互服务,iOS原生实现
*
* 负责:
* 1) 监听蓝牙耳机按键
* 2) 处理语音识别
* 3) 与火山AI服务交互
* 4) 文本转语音播放
*/
@available(iOS 13.0, *)
class VoiceInteractionService: NSObject {
// 常量
private let TAG = "VoiceInteractionService"
private let RECOGNITION_TIMEOUT: TimeInterval = 8.0
// 单例
static let shared = VoiceInteractionService()
// 服务状态
private var isRunning = false
private var isActive = false
private var isRecognitionActive = false
private var isTimeoutPaused = false
private var hasSpeechDetected = false
private var isTtsSpeaking = false
// 事件处理器
private weak var eventHandler: VoiceInteractionEventHandler?
// 按键处理
private var lastKeyEventTime: TimeInterval = 0
private var keyEventCount = 0
// 活动时间
private var lastActivityTime: TimeInterval = 0
// 当前用户输入
private var currentUserInput = ""
private var lastUserMessage = ""
private var lastAssistantMessage = ""
// 服务组件
private var azureAsrHelper: AzureAsrHelper?
private var azureTtsHelper: AzureTtsHelper?
private var volcanoAIService: VolcanoAIService?
private var silenceTimer: Timer?
// 系统提示词
private let systemPrompt = """
你是一个智能语音助手,能够简洁明了地回答用户的问题。
请保持回答简短、准确,避免过长的解释。
如果用户的问题不清楚,请礼貌地请求澄清。
不要使用复杂的术语,除非用户明确要求。
用户用语音和你交互。
"""
// 初始化方法
private override init() {
super.init()
NSLog("%@: VoiceInteractionService 初始化中", TAG)
}
/**
* 启动服务
*
* @param apiKey Azure语音服务API密钥
* @param region Azure语音服务区域
* @param volcanoKey 火山AI服务API密钥
* @return 启动是否成功
*/
func start(azureKey: String, azureRegion: String, volcanoKey: String) -> Bool {
guard !isRunning else {
NSLog("%@: 服务已经在运行", TAG)
return true
}
NSLog("%@: 启动服务中", TAG)
// 重置状态
resetState()
// 初始化各个服务组件
initServices(azureKey: azureKey, azureRegion: azureRegion, volcanoKey: volcanoKey)
// 启动monitorService定时器
silenceTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(monitorService), userInfo: nil, repeats: true)
NSLog("%@: monitorService定时器已启动", TAG)
// 标记服务为运行状态
isRunning = true
NSLog("%@: 服务启动完成,等待蓝牙按键事件", TAG)
return true
}
/**
* 停止服务
*/
func stop() {
guard isRunning else {
NSLog("%@: 服务未运行", TAG)
return
}
NSLog("%@: 停止服务中", TAG)
// 停止monitorService定时器
silenceTimer?.invalidate()
silenceTimer = nil
// 停止语音识别
if isRecognitionActive {
stopVoiceRecognition()
}
// 停止TTS
stopCurrentTTS()
// 释放服务实例
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
// 更新状态
resetState()
isRunning = false
NSLog("%@: 服务已停止", TAG)
}
/**
* 获取服务运行状态
*/
func isServiceRunning() -> Bool {
return isRunning
}
/**
* 重置状态
*/
private func resetState() {
isActive = false
isRecognitionActive = false
isTimeoutPaused = false
hasSpeechDetected = false
isTtsSpeaking = false
lastUserMessage = ""
lastAssistantMessage = ""
}
/**
* 初始化服务
*/
private func initServices(azureKey: String, azureRegion: String, volcanoKey: String) {
NSLog("%@: 初始化服务组件", TAG)
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper()
if let helper = azureAsrHelper {
helper.setEventHandler({ [weak self] eventData in
// 处理ASR事件
self?.handleAsrEvent(eventData: eventData)
})
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper()
if let helper = azureTtsHelper {
helper.setEventHandler({ [weak self] eventData in
// 处理TTS事件
self?.handleTtsEvent(eventData: eventData)
})
}
// 初始化火山AI服务
volcanoAIService = VolcanoAIService()
// 配置Azure ASR
if !azureKey.isEmpty && !azureRegion.isEmpty {
azureAsrHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, supportedLanguages: ["zh-CN"])
NSLog("%@: Azure ASR初始化完成", TAG)
} else {
NSLog("%@: Azure配置信息不完整,无法初始化Azure ASR", TAG)
}
// 配置Azure TTS
if !azureKey.isEmpty && !azureRegion.isEmpty {
azureTtsHelper?.initialize(speechSubscriptionKey: azureKey, serviceRegion: azureRegion, language: "zh-CN")
NSLog("%@: Azure TTS初始化完成", TAG)
} else {
NSLog("%@: Azure配置信息不完整,无法初始化Azure TTS", TAG)
}
// 配置火山AI
if !volcanoKey.isEmpty {
volcanoAIService?.initialize(apiKey: volcanoKey)
NSLog("%@: 火山AI服务初始化完成", TAG)
} else {
NSLog("%@: 火山AI配置信息不完整,无法初始化火山AI服务", TAG)
}
}
/**
* 监控服务状态
*/
@objc private func monitorService() {
// 确保服务保持活跃状态
if !isActive {
isActive = true
}
// 检查语音识别状态
if isRecognitionActive {
let currentTime = Date().timeIntervalSince1970
let elapsedTime = currentTime - lastActivityTime
// NSLog("%@: 已过去时间: %@, 是否检测到语音: %@, 是否TTS播放: %@", TAG, String(elapsedTime), String(hasSpeechDetected), String(isTtsSpeaking), String(elapsedTime))
// 如果超过超时时间没有检测到语音,且不在TTS播放中,暂停语音识别
if !hasSpeechDetected && !isTtsSpeaking && elapsedTime >= RECOGNITION_TIMEOUT {
// NSLog("%@: 超过%@秒未检测到语音,停止识别", TAG, String(RECOGNITION_TIMEOUT))
isTimeoutPaused = true
playNotification("没有听到您说话,已暂停对话。双击耳机按钮可重新开始。")
// 停止语音识别并确保资源完全释放
stopVoiceRecognition()
// 清理识别状态
isRecognitionActive = false
hasSpeechDetected = false
}
}
}
/**
* 处理ASR事件
*/
private func handleAsrEvent(eventData: [String: Any]) {
if let type = eventData["type"] as? String {
// NSLog("%@: 处理ASR事件: %@", TAG, type)
switch type {
case "recognizing":
if let text = eventData["text"] as? String, !text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
} else if let data = eventData["data"] as? [String: Any],
let text = data["text"] as? String,
!text.isEmpty {
hasSpeechDetected = true
stopCurrentTTS()
updateLastActivityTime()
}
case "result":
var text: String?
// 直接读取text或从data中读取
if let directText = eventData["text"] as? String, !directText.isEmpty {
text = directText
} else if let data = eventData["data"] as? [String: Any],
let dataText = data["text"] as? String,
!dataText.isEmpty {
text = dataText
}
if let finalText = text {
updateLastActivityTime()
// 保存用户消息
lastUserMessage = finalText
processWithVolcanoAI(finalText)
}
// 重置状态,继续识别
hasSpeechDetected = false
case "sessionStarted":
updateLastActivityTime()
case "sessionStopped":
isRecognitionActive = false
case "canceled":
isRecognitionActive = false
case "error":
isRecognitionActive = false
if let message = eventData["message"] as? String {
NSLog("%@: ASR错误: %@", TAG, message)
}
playNotification("语音识别出错")
default:
break
}
}
}
/**
* 处理TTS事件
*/
private func handleTtsEvent(eventData: [String: Any]) {
if let type = eventData["type"] as? String {
// NSLog("%@: 处理TTS事件: %@", TAG, type)
switch type {
case "started":
isTtsSpeaking = true
case "completed":
isTtsSpeaking = false
updateLastActivityTime()
case "canceled":
isTtsSpeaking = false
case "error":
isTtsSpeaking = false
if let message = eventData["message"] as? String {
NSLog("%@: TTS错误: %@", TAG, message)
}
default:
break
}
}
}
/**
* 处理双击事件
*/
func handleMediaButtonAction() {
NSLog("%@: 处理媒体按钮事件", TAG)
// 屏蔽短时间重复响应的问题
let currentTime = Date().timeIntervalSince1970
let timeDiff = currentTime - lastKeyEventTime
if timeDiff < 0.5 {
NSLog("%@: 短时间重复响应,忽略", TAG)
return
}
lastKeyEventTime = currentTime
// 停止当前TTS播放
stopCurrentTTS()
// 检查应用是否在后台
if UIApplication.shared.applicationState == .background {
NSLog("%@: 应用在后台,无法开始语音识别", TAG)
playNotification("请打开应用后重试!")
return
}
// 播放提示音
playPrompt("我在!")
// 重置超时暂停标志
isTimeoutPaused = false
// 启动或重置语音识别
if !isRecognitionActive {
NSLog("%@: 语音识别未激活,开始启动", TAG)
startVoiceRecognition()
} else {
NSLog("%@: 语音识别已激活,更新活动时间", TAG)
updateLastActivityTime()
hasSpeechDetected = false
}
}
/**
* 开始语音识别
*/
private func startVoiceRecognition() {
if isRecognitionActive { return }
// 通知Flutter语音识别已启动
notifyVoiceRecognitionStarted()
isActive = true
isRecognitionActive = true
hasSpeechDetected = false
updateLastActivityTime()
if let asrHelper = azureAsrHelper {
let success = asrHelper.startContinuousRecognition()
if !success {
isRecognitionActive = false
NSLog("%@: 启动语音识别失败", TAG)
playNotification("启动语音识别失败")
}
} else {
isRecognitionActive = false
NSLog("%@: Azure ASR服务未初始化", TAG)
playNotification("语音识别服务未初始化")
}
}
/**
* 停止语音识别
*/
private func stopVoiceRecognition() {
if !isRecognitionActive { return }
NSLog("%@: 停止语音识别", TAG)
if let asrHelper = azureAsrHelper {
let success = asrHelper.stopContinuousRecognition()
if !success {
NSLog("%@: 停止语音识别失败", TAG)
}
}
isRecognitionActive = false
hasSpeechDetected = false
}
/**
* 使用火山AI处理语音识别结果
*/
private func processWithVolcanoAI(_ text: String) {
// 保存当前用户输入
currentUserInput = text
// 在后台线程处理
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self, let volcanoAIService = self.volcanoAIService else {
return
}
do {
// 创建消息
let message = volcanoAIService.createUserMessage(content: text)
let messages = [message]
// 发送请求
let response = try volcanoAIService.sendMessage(messages: messages, systemPrompt: self.systemPrompt)
// 保存助手回复
self.lastAssistantMessage = response
// 播放AI回复
self.speakAIResponse(response)
// 同步聊天记录到Flutter端
self.notifyChatHistoryUpdated(userMessage: text, assistantMessage: response)
} catch {
NSLog("%@: AI处理出错: %@", self.TAG, error.localizedDescription)
self.playNotification("AI处理出错")
}
}
}
/**
* 播放AI回复
*/
private func speakAIResponse(_ text: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: text)
}
/**
* 播放提示音
*/
private func playPrompt(_ message: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: message)
}
/**
* 播放通知提示音
*/
private func playNotification(_ message: String) {
isTtsSpeaking = true
azureTtsHelper?.speakText(text: message)
}
/**
* 停止当前TTS播放
*/
private func stopCurrentTTS() {
if isTtsSpeaking {
azureTtsHelper?.stopSpeaking()
isTtsSpeaking = false
}
}
/**
* 更新最后活动时间
*/
private func updateLastActivityTime() {
lastActivityTime = Date().timeIntervalSince1970
}
/**
* 暂停语音交互
*/
func pauseVoiceInteraction() {
NSLog("%@: 暂停语音交互", TAG)
// 停止当前语音播放
stopCurrentTTS()
// 停止语音识别
if isRecognitionActive {
stopVoiceRecognition()
}
// 设置状态为暂停,但服务保持运行
isTimeoutPaused = true
}
/**
* 通知语音识别已开始
*/
private func notifyVoiceRecognitionStarted() {
// 发送识别开始事件
sendEvent(type: "recognition_started", data: [:])
}
/**
* 通知聊天历史更新
*/
private func notifyChatHistoryUpdated(userMessage: String, assistantMessage: String) {
// 发送聊天历史更新事件
sendEvent(type: "chat_history_updated", data: [
"agentId": "personal_assistant",
"userMessage": userMessage,
"assistantMessage": assistantMessage
])
}
/**
* 设置事件处理器
*/
func setEventHandler(_ handler: VoiceInteractionEventHandler) {
self.eventHandler = handler
}
/**
* 发送事件到Flutter端
*/
private func sendEvent(type: String, data: [String: Any] = [:]) {
var eventData = data
eventData["type"] = type
eventData["timestamp"] = Int(Date().timeIntervalSince1970 * 1000)
// 使用事件处理器发送事件
eventHandler?.sendEvent(eventData)
}
}

391
ios/Runner/VolcanoAIService.swift

@ -1,391 +0,0 @@
import Foundation
/**
* 火山AI服务的iOS原生实现
*
* 参考Android端的VolcanoAIService实现,提供同步和异步的API调用方式
*/
class VolcanoAIService {
private let TAG = "VolcanoAIService"
private let baseUrl = "https://ark.cn-beijing.volces.com/api/v3"
private let chatEndpoint = "/chat/completions"
private let session: URLSession
private var apiKey: String = ""
private var isInitialized = false
init() {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30.0
config.timeoutIntervalForResource = 30.0
self.session = URLSession(configuration: config)
}
/**
* 初始化火山AI服务
*
* @param apiKey 火山AI API密钥
* @return 初始化是否成功
*/
func initialize(apiKey: String) -> Bool {
self.apiKey = apiKey
isInitialized = !apiKey.isEmpty
if !isInitialized {
NSLog("%@: 初始化失败:API key 不能为空", TAG)
} else {
NSLog("%@: 火山AI服务初始化成功", TAG)
}
return isInitialized
}
/**
* 生成个性化问候语
*
* @param agentName 代理名称
* @param systemPrompt 系统提示词
* @param callback 回调函数,返回生成的问候语
*/
func generateGreeting(agentName: String, systemPrompt: String, callback: @escaping (String?, Error?) -> Void) {
let messages: [[String: Any]] = [
["role": "system", "content": systemPrompt],
["role": "user", "content": "请用一句简短的话向我打个招呼,要符合你的身份和性格特点,不要超过18个字。"]
]
let messagesData = try? JSONSerialization.data(withJSONObject: messages, options: [])
let messagesArray = try? JSONSerialization.jsonObject(with: messagesData!, options: []) as? [[String: Any]]
var result = ""
sendMessageStream(messages: messagesArray!, systemPrompt: systemPrompt, streamCallback: StreamCallback(
onToken: { token in
result.append(token)
},
onComplete: {
callback(result, nil)
},
onError: { error in
callback(nil, error)
}
))
}
/**
* 发送消息(非流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回AI的回复
* @throws VolcanoAIError 如果API调用失败
*/
func sendMessage(messages: [[String: Any]], systemPrompt: String) throws -> String {
// 检查是否已初始化
if !isInitialized || apiKey.isEmpty {
throw VolcanoAIError.serviceNotInitialized
}
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
let requestBody: [String: Any] = [
"model": "doubao-1-5-lite-32k-250115",
"messages": fullMessages,
"temperature": 0.7,
"max_tokens": 2000,
"stream": false
]
guard let url = URL(string: "\(baseUrl)\(chatEndpoint)") else {
throw VolcanoAIError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
} catch {
throw VolcanoAIError.invalidRequestBody
}
let semaphore = DispatchSemaphore(value: 0)
var responseData: Data?
var responseError: Error?
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
responseError = VolcanoAIError.networkError(error.localizedDescription)
semaphore.signal()
return
}
guard let httpResponse = response as? HTTPURLResponse else {
responseError = VolcanoAIError.invalidResponse
semaphore.signal()
return
}
if !(200...299).contains(httpResponse.statusCode) {
var errorMessage = "Unknown error occurred"
if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let error = json["error"] as? [String: Any],
let message = error["message"] as? String {
errorMessage = message
}
responseError = VolcanoAIError.apiError(errorMessage)
semaphore.signal()
return
}
responseData = data
semaphore.signal()
}
task.resume()
_ = semaphore.wait(timeout: .distantFuture)
if let error = responseError {
throw error
}
guard let data = responseData else {
throw VolcanoAIError.emptyResponse
}
do {
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let choices = json["choices"] as? [[String: Any]],
let firstChoice = choices.first,
let message = firstChoice["message"] as? [String: Any],
let content = message["content"] as? String else {
throw VolcanoAIError.invalidResponseFormat
}
return content
} catch {
throw VolcanoAIError.invalidResponseFormat
}
}
/**
* 发送消息(流式输出)
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @param streamCallback 回调函数,用于接收流式输出的结果
*/
func sendMessageStream(messages: [[String: Any]], systemPrompt: String, streamCallback: StreamCallback) {
// 检查是否已初始化
if !isInitialized || apiKey.isEmpty {
streamCallback.onError(VolcanoAIError.serviceNotInitialized)
return
}
var fullMessages: [[String: Any]] = [
["role": "system", "content": systemPrompt]
]
fullMessages.append(contentsOf: messages)
let requestBody: [String: Any] = [
"model": "doubao-1-5-lite-32k-250115",
"messages": fullMessages,
"temperature": 0.7,
"max_tokens": 2000,
"stream": true
]
guard let url = URL(string: "\(baseUrl)\(chatEndpoint)") else {
streamCallback.onError(VolcanoAIError.invalidURL)
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
request.addValue("text/event-stream", forHTTPHeaderField: "Accept")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: requestBody, options: [])
} catch {
streamCallback.onError(VolcanoAIError.invalidRequestBody)
return
}
let task = session.dataTask(with: request) { data, response, error in
if let error = error {
streamCallback.onError(VolcanoAIError.networkError(error.localizedDescription))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
streamCallback.onError(VolcanoAIError.invalidResponse)
return
}
if !(200...299).contains(httpResponse.statusCode) {
var errorMessage = "Unknown error occurred"
if let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let error = json["error"] as? [String: Any],
let message = error["message"] as? String {
errorMessage = message
}
streamCallback.onError(VolcanoAIError.apiError(errorMessage))
return
}
guard let data = data else {
streamCallback.onError(VolcanoAIError.emptyResponse)
return
}
// 处理SSE流数据
let responseString = String(data: data, encoding: .utf8) ?? ""
let lines = responseString.components(separatedBy: "\n")
for line in lines {
if line.isEmpty { continue }
if line.hasPrefix("data: ") {
let data = String(line.dropFirst(6))
if data == "[DONE]" {
streamCallback.onComplete()
break
}
do {
if let jsonData = data.data(using: .utf8),
let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
let choices = json["choices"] as? [[String: Any]],
let firstChoice = choices.first,
let delta = firstChoice["delta"] as? [String: Any],
let content = delta["content"] as? String {
streamCallback.onToken(content)
}
} catch {
// 忽略无效的JSON数据
continue
}
}
}
}
task.resume()
}
/**
* 同步方式发送消息(流式输出)
*
* 注意:此方法会阻塞当前线程,请在后台线程中调用
*
* @param messages 消息列表
* @param systemPrompt 系统提示词
* @return 返回完整的AI回复
* @throws VolcanoAIError 如果API调用失败
*/
func sendMessageStreamSync(messages: [[String: Any]], systemPrompt: String) throws -> String {
var result = ""
let semaphore = DispatchSemaphore(value: 0)
var responseError: Error?
sendMessageStream(messages: messages, systemPrompt: systemPrompt, streamCallback: StreamCallback(
onToken: { token in
result.append(token)
},
onComplete: {
semaphore.signal()
},
onError: { error in
responseError = error
semaphore.signal()
}
))
// 等待流式输出完成或出错
_ = semaphore.wait(timeout: .now() + 60)
if let error = responseError {
throw error
}
return result
}
/**
* 创建用户消息
*/
func createUserMessage(content: String) -> [String: Any] {
return ["role": "user", "content": content]
}
/**
* 创建系统消息
*/
func createSystemMessage(content: String) -> [String: Any] {
return ["role": "system", "content": content]
}
/**
* 创建助手消息
*/
func createAssistantMessage(content: String) -> [String: Any] {
return ["role": "assistant", "content": content]
}
/**
* 流式输出回调类
*/
class StreamCallback {
let onToken: (String) -> Void
let onComplete: () -> Void
let onError: (Error) -> Void
init(onToken: @escaping (String) -> Void, onComplete: @escaping () -> Void, onError: @escaping (Error) -> Void) {
self.onToken = onToken
self.onComplete = onComplete
self.onError = onError
}
}
}
/**
* 火山AI错误枚举
*/
enum VolcanoAIError: Error {
case serviceNotInitialized
case invalidURL
case invalidRequestBody
case networkError(String)
case invalidResponse
case emptyResponse
case invalidResponseFormat
case apiError(String)
var localizedDescription: String {
switch self {
case .serviceNotInitialized:
return "火山AI服务未初始化或API key为空,请先调用initialize方法"
case .invalidURL:
return "无效的URL"
case .invalidRequestBody:
return "无效的请求体"
case .networkError(let message):
return "网络错误: \(message)"
case .invalidResponse:
return "无效的响应"
case .emptyResponse:
return "空响应"
case .invalidResponseFormat:
return "无效的响应格式"
case .apiError(let message):
return "API错误: \(message)"
}
}
}

BIN
ios/Runner/sample.mp3

Binary file not shown.

BIN
ios/Runner/silence.mp3

Binary file not shown.

34
lib/core/bindings/initial_binding.dart

@ -1,12 +1,11 @@
import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:get/get.dart';
import '../../data/services/speech_factory.dart';
import '../../data/services/chat_history_service.dart';
import '../../data/services/volcano_translation_service.dart';
import '../../data/services/language_manager.dart';
import '../../data/services/volcano_ai_service.dart';
import '../../core/utils/logger.dart';
import '../../data/services/open_ai_service_adapter.dart';
import '../../data/services/bluetooth_manager.dart';
import '../../data/services/ble_manager.dart';
@ -17,10 +16,11 @@ class InitialBinding extends Bindings {
Logger.warning('InitialBinding dependencies');
// 语言管理器(需要最先初始化)
Get.put<LanguageManager>(LanguageManager(), permanent: true);
// 语言管理器(需要最先初始化)
Get.lazyPut<LanguageManager>(() => LanguageManager(), fenix: true);
//初始化语音工厂
Get.put<SpeechFactory>(SpeechFactory(), permanent: true);
Get.lazyPut<SpeechFactory>(() => SpeechFactory(), fenix: true);
Get.find<SpeechFactory>().initialize(initialType: SpeechServiceType.azure);
// 历史记录服务
@ -30,30 +30,16 @@ class InitialBinding extends Bindings {
Get.lazyPut<VolcanoTranslationService>(() => VolcanoTranslationService(),
fenix: true);
// OpenAI服务适配器
Get.lazyPut<OpenAIServiceAdapter>(() {
final adapter = OpenAIServiceAdapter();
adapter.initialize();
return adapter;
}, fenix: true);
// 蓝牙服务
Get.put<BluetoothManager>(BluetoothManager(), permanent: true);
Get.lazyPut<BluetoothManager>(() => BluetoothManager(), fenix: true);
// BleManager
Get.put<BleManager>(BleManager(), permanent: true);
Get.lazyPut<BleManager>(() => BleManager(), fenix: true);
// MusicManager
Get.put<MusicManager>(MusicManager(), permanent: true);
Get.lazyPut<MusicManager>(() => MusicManager(), fenix: true);
// NavigationManager
Get.lazyPut<NavigationManager>(() => NavigationManager(), fenix: true);
// 火山AI服务
Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 蓝牙媒体按钮服务
// Get.lazyPut<BluetoothMediaButtonService>(
// () => BluetoothMediaButtonService(),
// fenix: true);
// // 语音交互服务
// Get.lazyPut<VoiceInteractionService>(() => VoiceInteractionService(),
// fenix: true);
// Get.lazyPut<VolcanoAIService>(() => VolcanoAIService(), fenix: true);
// 杰理蓝牙服务

4
lib/core/themes/theme_service.dart

@ -21,7 +21,7 @@ class ThemeService {
Logger.info('从存储读取主题设置: $value');
return value ?? false;
} catch (e) {
Logger.error('读取主题设置失败: $e');
Logger.error('读取主题设置失败: ${e.toString()}');
return false;
}
}
@ -32,7 +32,7 @@ class ThemeService {
Logger.info('保存主题设置: ${isDarkMode ? "暗黑" : "浅色"}');
await _box.write(_key, isDarkMode);
} catch (e) {
Logger.error('保存主题设置失败: $e');
Logger.error('保存主题设置失败: ${e.toString()}');
}
}
}

6
lib/core/translations/language/zh_cn.dart

@ -332,6 +332,12 @@ const Map<String, String> zhCN = {
"你好!我是 Emma,很高兴成为你的私人助理。我可以帮你处理各种任务,比如时间管理、目标规划、信息整理等。有什么我可以帮你的吗?",
"personalAssistantVoiceWelcome": "我在呢!",
// 导航
"driving": "驾车",
"walking": "步行",
"bicycling": "骑行",
"tsransit": "公交",
// 同声传译 Lucy
"interpreterName": "同声传译",
"interpreterDescription":

85
lib/core/utils/logger.dart

@ -15,77 +15,84 @@ class Logger {
/// 当前日志级别,低于此级别的日志不会被输出
static LogLevel _currentLevel = LogLevel.debug;
/// 默认标签
static const String _defaultTag = 'App';
/// 设置日志级别
static void setLevel(LogLevel level) {
_currentLevel = level;
}
/// 输出调试日志 (不带标签版本)
static void debug(String message) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', _defaultTag, message);
}
}
/// 输出调试日志
static void debug(String message, {String? tag}) {
static void d(String tag, String message) {
if (_currentLevel.index <= LogLevel.debug.index) {
_log('DEBUG', message, tag: tag);
_log('DEBUG', tag, message);
}
}
/// 输出信息日志 (不带标签版本)
static void info(String message) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', _defaultTag, message);
}
}
/// 输出信息日志
static void info(String message, {String? tag}) {
static void i(String tag, String message) {
if (_currentLevel.index <= LogLevel.info.index) {
_log('INFO', message, tag: tag);
_log('INFO', tag, message);
}
}
/// 输出警告日志 (不带标签版本)
static void warning(String message) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', _defaultTag, message);
}
}
/// 输出警告日志
static void warning(String message, {String? tag}) {
static void w(String tag, String message) {
if (_currentLevel.index <= LogLevel.warning.index) {
_log('WARNING', message, tag: tag);
_log('WARNING', tag, message);
}
}
/// 输出错误日志
static void error(String message, [dynamic error, StackTrace? stackTrace, String? tag]) {
/// 输出错误日志 (不带标签版本)
static void error(String message, [dynamic error, StackTrace? stackTrace]) {
if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', message, tag: tag);
_log('ERROR', _defaultTag, message);
if (error != null) {
_log('ERROR', 'Original error: $error', tag: tag);
_log('ERROR', _defaultTag, 'Original error: $error');
}
if (stackTrace != null) {
_log('ERROR', 'Stack trace: $stackTrace', tag: tag);
_log('ERROR', _defaultTag, 'Stack trace: $stackTrace');
}
}
}
/// 获取当前调用栈的类名
static String? _getCallerClassName() {
try {
final stackTrace = StackTrace.current.toString();
final frames = stackTrace.split('\n');
// 查找调用者帧 (跳过Logger框架内部的帧)
for (int i = 0; i < frames.length; i++) {
final frame = frames[i].trim();
if (frame.contains('Logger.') || frame.contains('_log')) {
continue;
}
// 简单的解析类名
final regex = RegExp(r'#\d+\s+([^\.]+)\.([^\.]+)\s');
final match = regex.firstMatch(frame);
if (match != null && match.groupCount >= 1) {
return match.group(1);
}
break;
/// 输出错误日志
static void e(String tag, String message, [dynamic error, StackTrace? stackTrace]) {
if (_currentLevel.index <= LogLevel.error.index) {
_log('ERROR', tag, message);
if (error != null) {
_log('ERROR', tag, 'Original error: $error');
}
if (stackTrace != null) {
_log('ERROR', tag, 'Stack trace: $stackTrace');
}
} catch (e) {
// 忽略解析错误
}
return null;
}
/// 内部日志输出方法
static void _log(String level, String message, {String? tag}) {
final timestamp = DateTime.now().toString();
final className = tag ?? _getCallerClassName() ?? '';
final tagInfo = className.isNotEmpty ? '[$className] ' : '';
developer.log('[$timestamp][$level][$tagInfo]: $message');
static void _log(String level, String tag, String message) {
developer.log('[$tag]: $message');
}
}

48
lib/data/services/ble_manager.dart

@ -54,7 +54,7 @@ class DeviceInfo {
this.rightEarCharging = false,
this.caseCharging = false,
this.firmwareVersion = '',
this.firmwareProductId = 's3',
this.firmwareProductId = '',
this.firmwareProductColor = '',
DateTime? lastUpdated,
}) : lastUpdated = lastUpdated ?? DateTime.now();
@ -168,7 +168,7 @@ class BleManager extends GetxService {
Future.delayed(const Duration(milliseconds: 300), () {
// 可选:检查是否仍处于连接状态
if (_isConnected.value) {
fetchDeviceInfo();
// fetchDeviceInfo();
}
});
} else if (state == BleConnectionState.disconnected ||
@ -198,7 +198,7 @@ class BleManager extends GetxService {
},
);
} catch (e) {
Logger.error('设置BLE事件监听器失败: $e');
Logger.error('设置BLE事件监听器失败: ${e.toString()}');
}
}
@ -225,7 +225,7 @@ class BleManager extends GetxService {
}
}
} catch (e) {
Logger.error('处理设备信息异常: $e');
Logger.error('处理设备信息异常: ${e.toString()}');
}
}
@ -306,7 +306,7 @@ class BleManager extends GetxService {
Logger.info(
'设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
} catch (e) {
Logger.error('更新电池信息失败: $e');
Logger.error('更新电池信息失败: ${e.toString()}');
}
}
@ -329,7 +329,7 @@ class BleManager extends GetxService {
Logger.info('设备版本信息已更新: 版本=$version');
} catch (e) {
Logger.error('更新版本信息失败: $e');
Logger.error('更新版本信息失败: ${e.toString()}');
}
}
@ -352,7 +352,7 @@ class BleManager extends GetxService {
Logger.info('设备设备id已更新: 设备id=$productId');
} catch (e) {
Logger.error('更新设备id失败: $e');
Logger.error('更新设备id失败: ${e.toString()}');
}
}
@ -440,7 +440,7 @@ class BleManager extends GetxService {
Logger.warning('产品id获取指令发送失败');
}
} catch (e) {
Logger.error('获取BLE设备信息失败: $e');
Logger.error('获取BLE设备信息失败: ${e.toString()}');
}
}
@ -455,7 +455,7 @@ class BleManager extends GetxService {
return _connectionState.value;
} catch (e) {
Logger.error('获取BLE连接状态失败: $e');
Logger.error('获取BLE连接状态失败: ${e.toString()}');
return BleConnectionState.error;
}
}
@ -483,7 +483,9 @@ class BleManager extends GetxService {
final openaiBaseUrl =
AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1';
final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo';
final systemPrompt = AppConfig.agentsystemPrompt("personal_assistant");
final openaiagentid =
AppConfig.env('OPENAI_AGENT_ID') ?? 'personal_assistant';
final systemPrompt = AppConfig.agentsystemPrompt(openaiagentid);
// 从Flutter资源包中加载.mcp.json
final jsonData = AppConfig.mcpConfig();
@ -497,6 +499,8 @@ class BleManager extends GetxService {
'openaiModel': openaiModel,
'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData),
'volcanoAppId': '2316081933',
'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En',
};
// 记录日志(不记录敏感信息)
@ -531,7 +535,7 @@ class BleManager extends GetxService {
// 如果设备信息存在,则获取详细信息
if (deviceInfo.value != null) {
// 获取详细设备信息
fetchDeviceInfo();
// fetchDeviceInfo();
}
}
} else {
@ -540,7 +544,7 @@ class BleManager extends GetxService {
return result;
} catch (e) {
Logger.error('启动BLE服务失败: $e');
Logger.error('启动BLE服务失败: ${e.toString()}');
return false;
}
}
@ -560,7 +564,7 @@ class BleManager extends GetxService {
return result;
} catch (e) {
Logger.error('停止BLE服务失败: $e');
Logger.error('停止BLE服务失败: ${e.toString()}');
return false;
}
}
@ -570,7 +574,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getBatteryLevel();
} catch (e) {
Logger.error('获取电池电量失败: $e');
Logger.error('获取电池电量失败: ${e.toString()}');
return 0;
}
}
@ -580,7 +584,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getBatteryInfo();
} catch (e) {
Logger.error('获取电池信息失败: $e');
Logger.error('获取电池信息失败: ${e.toString()}');
return false;
}
}
@ -590,7 +594,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getProductId();
} catch (e) {
Logger.error('获取设备版本失败: $e');
Logger.error('获取设备版本失败: ${e.toString()}');
return false;
}
}
@ -600,7 +604,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getDeviceVersion();
} catch (e) {
Logger.error('获取设备版本失败: $e');
Logger.error('获取设备版本失败: ${e.toString()}');
return false;
}
}
@ -610,7 +614,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openEncoder();
} catch (e) {
Logger.error('打开编码器失败: $e');
Logger.error('打开编码器失败: ${e.toString()}');
return false;
}
}
@ -620,7 +624,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
Logger.error('打开解码器失败: ${e.toString()}');
return false;
}
}
@ -630,7 +634,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openA2DPDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
Logger.error('打开解码器失败: ${e.toString()}');
return false;
}
}
@ -640,7 +644,7 @@ class BleManager extends GetxService {
try {
return await _bleService.closeCodec();
} catch (e) {
Logger.error('关闭编解码器失败: $e');
Logger.error('关闭编解码器失败: ${e.toString()}');
return false;
}
}
@ -650,7 +654,7 @@ class BleManager extends GetxService {
try {
return await _bleService.playA2DP();
} catch (e) {
Logger.error('播放A2DP失败: $e');
Logger.error('播放A2DP失败: ${e.toString()}');
return false;
}
}

8
lib/data/services/bluetooth_manager.dart

@ -75,11 +75,11 @@ class BluetoothManager extends GetxService {
}
},
onError: (error) {
Logger.error('蓝牙事件流错误: $error');
Logger.error('蓝牙事件流错误: ${error.toString()}');
},
);
} catch (e) {
Logger.error('设置蓝牙事件监听器失败: $e');
Logger.error('设置蓝牙事件监听器失败: ${e.toString()}');
}
}
@ -106,7 +106,7 @@ class BluetoothManager extends GetxService {
Logger.info('未找到已连接的蓝牙设备');
} catch (e) {
Logger.error('刷新设备连接失败: $e');
Logger.error('刷新设备连接失败: ${e.toString()}');
}
}
@ -117,7 +117,7 @@ class BluetoothManager extends GetxService {
_bluetoothState.value = isEnabled ? 'on' : 'off';
return isEnabled;
} catch (e) {
Logger.error('检查蓝牙状态失败: $e');
Logger.error('检查蓝牙状态失败: ${e.toString()}');
return false;
}
}

2
lib/data/services/chat_history_service.dart

@ -39,7 +39,7 @@ class ChatHistoryService extends GetxService {
// 检查并清理过多的历史记录
await _cleanupOldMessages(agentId);
} catch (e) {
Logger.error('保存聊天记录失败', e);
Logger.error('保存聊天记录失败', e.toString());
}
}

4
lib/data/services/deapsound_ai_service.dart

@ -112,12 +112,12 @@ class DeapsoundAIService implements AiService {
}
}
} catch (e) {
Logger.error('JSON解析错误: $e');
Logger.error('JSON解析错误: ${e.toString()}');
}
}
}
} catch (e) {
Logger.error('请求异常: $e');
Logger.error('请求异常: ${e.toString()}');
} finally {
client.close();
}

6
lib/data/services/kouzi_ai_service.dart

@ -82,14 +82,14 @@ class KouziAIService implements AiService {
}
}
} catch (e) {
Logger.error('JSON解析错误: $e');
Logger.error('JSON解析错误: ${e.toString()}');
}
}
}
} catch (e) {
Logger.error('请求异常: $e');
Logger.error('请求异常: ${e.toString()}');
} finally {
client.close();
}
}
}
}

4
lib/data/services/login_service.dart

@ -8,6 +8,7 @@ class LoginService {
FlWX().register(
appSecret: '4cfde33bc22b6cc6b1f8b225dd97d255',
appId: 'wx5bbb7be9324f3e2d',
universalLink: 'https://yunqiinnovation.com/app/',
params: FlWXCallbackParams(onHttp: (url) async {
return '';
}));
@ -18,13 +19,12 @@ class LoginService {
Future<void> _initializeFirebase() async {
try {
// 尝试获取已存在的实例
Firebase.app('DeepSound');
Firebase.app();
print('已存在的 Firebase 实例被重用');
} catch (e) {
try {
// 如果不存在则初始化新实例
await Firebase.initializeApp(
name: 'DeepSound',
options: DefaultFirebaseOptions.currentPlatform,
);
print('Firebase 初始化成功');

10
lib/data/services/music_manager.dart

@ -1,6 +1,7 @@
import 'package:deep_voice/core/utils/logger.dart';
import 'package:get/get.dart';
import 'package:music_service/music_service.dart';
import 'package:permission_handler/permission_handler.dart';
//音乐播放管理
class MusicManager extends GetxService {
@ -13,7 +14,14 @@ class MusicManager extends GetxService {
@override
void onInit() {
super.onInit();
Logger.info('BLE管理器初始化');
requestNotificationPermission();
Logger.info('初始化权限请求');
}
Future<void> requestNotificationPermission() async {
if (await Permission.notification.isDenied) {
await Permission.notification.request();
}
}
/// 释放资源

41
lib/data/services/navigation_manager.dart

@ -0,0 +1,41 @@
import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:get/get.dart';
//音乐播放管理
class NavigationManager extends GetxService {
static NavigationManager get to => Get.find<NavigationManager>();
// 音乐服务实例
final AmapService _amapervice = AmapService();
/// 初始化
@override
void onInit() {
super.onInit();
Logger.info('导航服务初始化');
}
/// 释放资源
@override
void onClose() {
super.onClose();
}
Stream<AmapServiceEvent> events() {
return _amapervice.events;
}
//导航
Future<void> start(String org, String dis) async {
// await _amapervice.navigation(org, dis);
}
//导航
Future<void> open(
String start,
String end,
String ntype,
) async {
await _amapervice.open(start, end, ntype);
}
}

10
lib/data/services/network/api.dart

@ -38,12 +38,20 @@ class Api {
}
//更新用户设置
static updateUserSetting([params]) {
static updateUserSetting([dynamic params]) {
return DioManager().request(
NWMethod.post,
'/api/home/user_setting',
params: params,
);
// options: params is dio.FormData
// ? Options(
// headers: {
// 'Content-Type': 'multipart/form-data',
// },
// )
// : null,
// );
}
//注销账号

2
lib/data/services/network/dio_manager.dart

@ -31,7 +31,7 @@ class DioManager {
Future request<T>(
NWMethod method,
String path, {
Map? params,
Object? params,
Map<String, dynamic>? queryParameters,
}) async {
Response response = await dio.request(

4
lib/data/services/speech_factory.dart

@ -88,7 +88,7 @@ class SpeechFactory extends GetxService {
Logger.info(
'语音服务初始化完成,当前类型: ${_currentServiceType.toString().split('.').last}');
} catch (e) {
Logger.error('语音服务初始化失败: $e');
Logger.error('语音服务初始化失败: ${e.toString()}');
rethrow;
}
}
@ -108,4 +108,4 @@ class SpeechFactory extends GetxService {
}
return Get.find<TtsService>();
}
}
}

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

@ -106,7 +106,7 @@ class AzureAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别服务初始化${result ? '成功' : '失败'}');
return result;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e');
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false;
rethrow;
}
@ -144,7 +144,7 @@ class AzureAsrService extends GetxService implements AsrService {
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString());
}
}
@ -177,7 +177,7 @@ class AzureAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
Logger.error('开始连续语音识别失败: ${e.toString()}');
_cleanupEventStream();
rethrow;
}
@ -195,7 +195,7 @@ class AzureAsrService extends GetxService implements AsrService {
_cleanupEventStream();
return result;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
Logger.error('停止连续语音识别失败: ${e.toString()}');
_cleanupEventStream();
rethrow;
}
@ -214,7 +214,6 @@ class AzureAsrService extends GetxService implements AsrService {
final String eventType = eventMap['type'] as String? ?? '';
// 添加日志帮助调试
Logger.debug('收到语音识别事件: $eventType, 数据: $eventMap');
switch (eventType) {
case 'result':
@ -260,7 +259,7 @@ class AzureAsrService extends GetxService implements AsrService {
final String errorDetails = eventMap['errorDetails'] as String? ?? '';
if (reason.isNotEmpty || errorDetails.isNotEmpty) {
Logger.error('识别取消: $reason - $errorDetails');
Logger.error('识别取消: $reason - ${errorDetails.toString()}');
}
_eventStreamController?.add(RecognitionEvent(
@ -271,7 +270,7 @@ class AzureAsrService extends GetxService implements AsrService {
case 'error':
final String error = eventMap['message'] as String? ?? '';
Logger.error('识别错误: $error');
Logger.error('识别错误: ${error.toString()}');
_eventStreamController?.add(RecognitionEvent(
type: RecognitionEventType.error,
error: error,
@ -282,7 +281,7 @@ class AzureAsrService extends GetxService implements AsrService {
/// 处理识别事件流错误
void _handleRecognitionError(Object error) {
Logger.error('识别事件流错误: $error');
Logger.error('识别事件流错误: ${error.toString()}');
_eventStreamController?.addError(error);
_cleanupEventStream();
}
@ -311,7 +310,7 @@ class AzureAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
Logger.error('释放语音识别资源失败: ${e.toString()}');
_cleanupEventStream();
_isInitialized = false;
}
@ -339,8 +338,8 @@ class AzureAsrService extends GetxService implements AsrService {
return true;
} catch (e) {
Logger.error('推送音频数据失败: $e');
Logger.error('推送音频数据失败: ${e.toString()}');
return false;
}
}
}
}

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

@ -69,7 +69,7 @@ class AzureTtsService extends GetxService implements TtsService {
_handleTtsEvent(event);
}
}, onError: (error) {
Logger.error('TTS事件流错误: $error');
Logger.error('TTS事件流错误: ${error.toString()}');
_eventStreamController.addError(error);
});
}
@ -78,7 +78,7 @@ class AzureTtsService extends GetxService implements TtsService {
void _handleTtsEvent(Map<dynamic, dynamic> event) {
final String eventType = event['type'] as String? ?? '';
// Logger.debug('收到TTS事件: $eventType, 数据: $event');
// Logger.debug('收到TTS事件: ${e.toString()}ventType, 数据: ${e.toString()}vent');
switch (eventType) {
case 'synthesis_started':
@ -159,7 +159,7 @@ class AzureTtsService extends GetxService implements TtsService {
_isInitialized = result;
return result;
} catch (e) {
Logger.error('初始化失败: $e');
Logger.error('初始化失败: ${e.toString()}');
return false;
}
}
@ -177,7 +177,7 @@ class AzureTtsService extends GetxService implements TtsService {
if (result) _currentVoice = voiceName;
return result;
} catch (e) {
Logger.error('设置语音失败: $e');
Logger.error('设置语音失败: ${e.toString()}');
return false;
}
}
@ -195,7 +195,7 @@ class AzureTtsService extends GetxService implements TtsService {
return result == true;
} catch (e) {
Logger.error('语音合成失败: $e');
Logger.error('语音合成失败: ${e.toString()}');
return false;
}
}
@ -250,7 +250,7 @@ class AzureTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
Logger.error('处理流式文本失败: ${e.toString()}');
return false;
}
}
@ -316,7 +316,7 @@ class AzureTtsService extends GetxService implements TtsService {
Logger.info('TTS队列播放完成');
return result;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
Logger.error('刷新TTS流失败: ${e.toString()}');
return false;
}
}
@ -358,7 +358,7 @@ class AzureTtsService extends GetxService implements TtsService {
_textQueue.removeAt(0);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
Logger.error('处理语音队列出错: ${e.toString()}');
// 出错时移除当前项并尝试继续
if (_textQueue.isNotEmpty) {
@ -389,7 +389,7 @@ class AzureTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
Logger.error('停止语音合成出错: ${e.toString()}');
return false;
}
}
@ -410,7 +410,7 @@ class AzureTtsService extends GetxService implements TtsService {
await _channel.invokeMethod('dispose');
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
Logger.error('释放资源失败: ${e.toString()}');
}
}
}
}

18
lib/data/services/speech_impl/flutter_asr_service.dart

@ -107,7 +107,7 @@ class FlutterAsrService extends GetxService implements AsrService {
} catch (e) {
_isAvailable.value = false;
_asrState.value = AsrState.error;
Logger.error('语音识别初始化失败: $e');
Logger.error('语音识别初始化失败: ${e.toString()}');
}
}
@ -203,7 +203,7 @@ class FlutterAsrService extends GetxService implements AsrService {
return result;
} catch (e) {
Logger.error('启动语音识别失败: $e');
Logger.error('启动语音识别失败: ${e.toString()}');
_asrState.value = AsrState.error;
return false;
}
@ -253,7 +253,7 @@ class FlutterAsrService extends GetxService implements AsrService {
?.add(RecognitionEvent.error('重启语音识别失败,可能存在系统资源冲突'));
}
} catch (e) {
Logger.error('重启连续识别失败: $e');
Logger.error('重启连续识别失败: ${e.toString()}');
// 发送错误给控制器
_continuousRecognitionController?.add(RecognitionEvent.error('重启失败: $e'));
@ -304,7 +304,7 @@ class FlutterAsrService extends GetxService implements AsrService {
await _initSpeech();
return _isAvailable.value;
} catch (e) {
Logger.error('语音识别初始化失败: $e');
Logger.error('语音识别初始化失败: ${e.toString()}');
return false;
}
}
@ -357,7 +357,7 @@ class FlutterAsrService extends GetxService implements AsrService {
},
);
} catch (e) {
Logger.error('一次性语音识别失败: $e');
Logger.error('一次性语音识别失败: ${e.toString()}');
return RecognitionEvent.error('$e');
}
}
@ -406,7 +406,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_isListening = true;
return _continuousRecognitionController!.stream;
} catch (e) {
Logger.error('启动连续语音识别失败: $e');
Logger.error('启动连续语音识别失败: ${e.toString()}');
// 如果已经创建了控制器,使用它
if (_continuousRecognitionController != null) {
@ -442,7 +442,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_asrState.value = AsrState.done;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
Logger.error('停止连续语音识别失败: ${e.toString()}');
// 强制重置状态
_isListening = false;
@ -472,7 +472,7 @@ class FlutterAsrService extends GetxService implements AsrService {
_isListening = false;
_asrState.value = AsrState.notInitialized;
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
Logger.error('释放语音识别资源失败: ${e.toString()}');
}
}
}
}

24
lib/data/services/speech_impl/flutter_tts_service.dart

@ -96,7 +96,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('FlutterTtsService 已初始化');
} catch (e) {
Logger.error('TTS初始化失败: $e');
Logger.error('TTS初始化失败: ${e.toString()}');
}
}
@ -118,7 +118,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('支持的语音: ${_supportedVoices.join(", ")}');
return true;
} catch (e) {
Logger.error('TTS初始化失败: $e');
Logger.error('TTS初始化失败: ${e.toString()}');
return false;
}
}
@ -149,7 +149,7 @@ class FlutterTtsService extends GetxService implements TtsService {
_currentVoice.value = _supportedVoices.first;
}
} catch (e) {
Logger.error('加载TTS语音失败: $e');
Logger.error('加载TTS语音失败: ${e.toString()}');
// 添加默认语音
if (_supportedVoices.isEmpty) {
@ -175,7 +175,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: $e');
Logger.error('设置TTS语音失败: ${e.toString()}');
return false;
}
}
@ -200,7 +200,7 @@ class FlutterTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('设置语音参数失败: $e');
Logger.error('设置语音参数失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('设置语音参数失败: $e'));
@ -227,7 +227,7 @@ class FlutterTtsService extends GetxService implements TtsService {
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
Logger.error('TTS播放失败: $e');
Logger.error('TTS播放失败: ${e.toString()}');
return false;
}
}
@ -288,7 +288,7 @@ class FlutterTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
@ -333,7 +333,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS队列播放完成');
return true;
} catch (e) {
Logger.error('等待TTS队列播放完成失败: $e');
Logger.error('等待TTS队列播放完成失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
@ -382,7 +382,7 @@ class FlutterTtsService extends GetxService implements TtsService {
}
}
} catch (e) {
Logger.error('TTS处理队列失败: $e');
Logger.error('TTS处理队列失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error(e.toString()));
@ -422,7 +422,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS停止播放完成');
return true;
} catch (e) {
Logger.error('TTS停止播放失败: $e');
Logger.error('TTS停止播放失败: ${e.toString()}');
// 即使出错也尝试强制重置状态
_isSpeaking.value = false;
@ -443,7 +443,7 @@ class FlutterTtsService extends GetxService implements TtsService {
Logger.info('TTS资源已释放');
} catch (e) {
Logger.error('释放TTS资源失败: $e');
Logger.error('释放TTS资源失败: ${e.toString()}');
}
}
@ -462,7 +462,7 @@ class FlutterTtsService extends GetxService implements TtsService {
await flushStream();
return true;
} catch (e) {
Logger.error('处理文本流时出错: $e');
Logger.error('处理文本流时出错: ${e.toString()}');
return false;
}
}

22
lib/data/services/speech_impl/voice_clone_tts_service.dart

@ -76,7 +76,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
);
return true;
} catch (e) {
Logger.error('TTS初始化失败: $e');
Logger.error('TTS初始化失败: ${e.toString()}');
return false;
}
}
@ -88,7 +88,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
Logger.info('设置TTS语音: $voiceName');
return true;
} catch (e) {
Logger.error('设置TTS语音失败: $e');
Logger.error('设置TTS语音失败: ${e.toString()}');
return false;
}
}
@ -127,7 +127,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
_handle(message);
},
onError: (error) {
Logger.error('连接失败: $error');
Logger.error('连接失败: ${error.toString()}');
},
onDone: () {
Logger.error('WebSocket 连接已关闭!');
@ -135,7 +135,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
cancelOnError: true,
);
} catch (e) {
Logger.error('初始化异常: $e');
Logger.error('初始化异常: ${e.toString()}');
}
}
@ -226,7 +226,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
_isSpeaking.value = false;
Logger.error('语音合成失败: $e');
Logger.error('语音合成失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('语音合成失败: $e'));
@ -285,7 +285,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('处理流式文本失败: $e');
Logger.error('处理流式文本失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('处理流式文本失败: $e'));
@ -319,7 +319,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
return true;
} catch (e) {
Logger.error('刷新TTS流失败: $e');
Logger.error('刷新TTS流失败: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('刷新TTS流失败: $e'));
@ -347,7 +347,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
await speakOnce(item);
}
} catch (e) {
Logger.error('处理语音队列出错: $e');
Logger.error('处理语音队列出错: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('处理语音队列出错: $e'));
@ -370,7 +370,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
}
return true;
} catch (e) {
Logger.error('停止语音合成出错: $e');
Logger.error('停止语音合成出错: ${e.toString()}');
// 发送错误事件
_eventController.add(TtsEvent.error('停止语音合成出错: $e'));
return false;
@ -387,7 +387,7 @@ class VoiceCloneTtsService extends GetxService implements TtsService {
await _eventController.close();
_isInitialized = false;
} catch (e) {
Logger.error('释放资源失败: $e');
Logger.error('释放资源失败: ${e.toString()}');
}
}
}
}

38
lib/data/services/speech_impl/volcano_asr_api_service.dart

@ -99,7 +99,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('火山语音API已初始化');
return true;
} catch (e) {
Logger.error('初始化火山语音API失败: $e');
Logger.error('初始化火山语音API失败: ${e.toString()}');
return false;
}
}
@ -151,7 +151,7 @@ class VolcanoAsrApiService implements AsrService {
return resultCompleter.future;
} catch (e) {
Logger.error('单次语音识别失败: $e');
Logger.error('单次语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString());
}
}
@ -184,7 +184,7 @@ class VolcanoAsrApiService implements AsrService {
return _recognitionController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
Logger.error('开始连续语音识别失败: ${e.toString()}');
await _cleanupResources();
// 返回一个包含错误的流
@ -222,7 +222,7 @@ class VolcanoAsrApiService implements AsrService {
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
Logger.error('停止连续语音识别失败: ${e.toString()}');
// 确保资源被清理
await _cleanupResources();
@ -250,7 +250,7 @@ class VolcanoAsrApiService implements AsrService {
_isInitialized = false;
Logger.info('火山语音API资源已释放');
} catch (e) {
Logger.error('释放火山语音API资源失败: $e');
Logger.error('释放火山语音API资源失败: ${e.toString()}');
_isContinuousRecognitionActive = false;
_isInitialized = false;
}
@ -284,7 +284,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('录音机初始化成功');
} catch (e) {
// _recorder = null;
Logger.error('初始化录音机失败: $e');
Logger.error('初始化录音机失败: ${e.toString()}');
rethrow;
}
}
@ -344,7 +344,7 @@ class VolcanoAsrApiService implements AsrService {
_connectionCompleter!.completeError(e);
}
Logger.error('连接WebSocket失败: $e');
Logger.error('连接WebSocket失败: ${e.toString()}');
rethrow;
}
@ -432,7 +432,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(heartbeatMsg));
} catch (e) {
Logger.error('发送心跳包失败: $e');
Logger.error('发送心跳包失败: ${e.toString()}');
}
}
}
@ -461,7 +461,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(startCommand));
Logger.info('已发送开始识别指令');
} catch (e) {
Logger.error('发送开始识别指令失败: $e');
Logger.error('发送开始识别指令失败: ${e.toString()}');
rethrow;
}
}
@ -496,7 +496,7 @@ class VolcanoAsrApiService implements AsrService {
Logger.info('录音已开始');
} catch (e) {
_isRecording = false;
Logger.error('开始录音失败: $e');
Logger.error('开始录音失败: ${e.toString()}');
rethrow;
}
*/
@ -544,7 +544,7 @@ class VolcanoAsrApiService implements AsrService {
await _sendAudioData(audioData);
}
} catch (e) {
Logger.error('捕获和发送音频失败: $e');
Logger.error('捕获和发送音频失败: ${e.toString()}');
}
*/
}
@ -562,7 +562,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(audioMessage));
} catch (e) {
Logger.error('发送音频数据失败: $e');
Logger.error('发送音频数据失败: ${e.toString()}');
}
}
@ -579,7 +579,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.add(jsonEncode(endCommand));
Logger.info('已发送结束指令');
} catch (e) {
Logger.error('发送结束指令失败: $e');
Logger.error('发送结束指令失败: ${e.toString()}');
}
}
@ -596,7 +596,7 @@ class VolcanoAsrApiService implements AsrService {
_isRecording = false;
Logger.info('录音已停止');
} catch (e) {
Logger.error('停止录音失败: $e');
Logger.error('停止录音失败: ${e.toString()}');
}
}
*/
@ -641,7 +641,7 @@ class VolcanoAsrApiService implements AsrService {
}
}
} catch (e) {
Logger.error('处理WebSocket消息失败: $e');
Logger.error('处理WebSocket消息失败: ${e.toString()}');
if (_recognitionController != null && !_recognitionController!.isClosed) {
_recognitionController!.addError('处理消息失败: $e');
@ -675,7 +675,7 @@ class VolcanoAsrApiService implements AsrService {
}
}
} catch (e) {
Logger.error('处理结果消息失败: $e');
Logger.error('处理结果消息失败: ${e.toString()}');
}
}
@ -741,7 +741,7 @@ class VolcanoAsrApiService implements AsrService {
/// 处理WebSocket错误
void _handleWebSocketError(dynamic error) {
Logger.error('WebSocket错误: $error');
Logger.error('WebSocket错误: ${e.toString()}rror');
if (_connectionCompleter != null && !_connectionCompleter!.isCompleted) {
_connectionCompleter!.completeError(error);
@ -789,7 +789,7 @@ class VolcanoAsrApiService implements AsrService {
_channel!.sink.close();
_channel = null;
} catch (e) {
Logger.error('关闭WebSocket连接失败: $e');
Logger.error('关闭WebSocket连接失败: ${e.toString()}');
}
}
}
@ -811,4 +811,4 @@ class VolcanoAsrApiService implements AsrService {
_isContinuousRecognitionActive = false;
_isRecognitionEndRequested = false;
}
}
}

16
lib/data/services/speech_impl/volcano_tts_api_service.dart

@ -357,7 +357,7 @@ class VolcanoTtsApiService extends GetxController {
_handleMessage,
onError: (error) {
// 直接处理错误,不尝试重连
Logger.error('WebSocket错误: $error');
Logger.error('WebSocket错误: ${error.toString()}');
if (!_eventController.isClosed) {
_eventController.add({
'eventType': 'error',
@ -391,7 +391,7 @@ class VolcanoTtsApiService extends GetxController {
});
}
} catch (e) {
Logger.error('处理TTS连接超时事件失败: $e');
Logger.error('处理TTS连接超时事件失败: ${e.toString()}');
}
}
timeout = null;
@ -427,7 +427,7 @@ class VolcanoTtsApiService extends GetxController {
return result;
} catch (e) {
// 只记录错误,不尝试重连
Logger.error('连接过程中发生异常: $e');
Logger.error('连接过程中发生异常: ${e.toString()}');
return false;
} finally {
isConnecting.value = false;
@ -479,7 +479,7 @@ class VolcanoTtsApiService extends GetxController {
'error': '会话开始超时'
});
} catch (e) {
Logger.error('处理TTS会话超时事件失败: $e');
Logger.error('处理TTS会话超时事件失败: ${e.toString()}');
}
}
timeout = null;
@ -515,7 +515,7 @@ class VolcanoTtsApiService extends GetxController {
return result;
} catch (e) {
// 只记录错误,不尝试恢复
Logger.error('开始TTS会话失败: $e');
Logger.error('开始TTS会话失败: ${e.toString()}');
return false;
}
}
@ -902,10 +902,10 @@ class VolcanoTtsApiService extends GetxController {
'error': error.toString()
});
Logger.error('火山语音TTS错误: $error');
Logger.error('火山语音TTS错误: ${error.toString()}');
} catch (e) {
// 如果在处理错误时发生异常,记录到日志但不再抛出异常
Logger.error('处理TTS错误时发生异常: $e,原始错误: $error');
Logger.error('处理TTS错误时发生异常: ${e.toString()},原始错误: ${error.toString()}');
}
}
@ -1445,7 +1445,7 @@ class VolcanoTtsApiService extends GetxController {
return success;
} catch (e) {
Logger.error('TTS一次性合成失败: $e');
Logger.error('TTS一次性合成失败: ${e.toString()}');
// 确保会话结束
endSession();
return false;

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

@ -87,11 +87,11 @@ class VolcanoTtsService extends GetxService implements TtsService {
case 'error':
_isSpeaking = false;
final errorCode = eventMap['errorCode'] as int? ?? 0;
final errorCode = eventMap['errorCode'] as String? ?? "-1";
final errorMessage = eventMap['errorMessage'] as String? ?? '未知错误';
_eventStreamController.add(TtsEvent(
type: TtsEventType.error,
error: errorMessage,
error: '$errorMessage (错误码: $errorCode)',
));
break;
}

20
lib/data/services/speech_impl/xunfei_asr_service.dart

@ -54,7 +54,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别服务初始化成功');
return true;
} catch (e) {
Logger.error('Azure 语音识别服务初始化失败: $e');
Logger.error('Azure 语音识别服务初始化失败: ${e.toString()}');
_isInitialized = false;
rethrow;
}
@ -69,7 +69,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
try {
throw Exception('无效的识别结果格式');
} catch (e) {
Logger.error('语音识别失败: $e');
Logger.error('语音识别失败: ${e.toString()}');
return RecognitionEvent.error(e.toString());
}
}
@ -112,7 +112,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = true;
return _eventStreamController!.stream;
} catch (e) {
Logger.error('开始连续语音识别失败: $e');
Logger.error('开始连续语音识别失败: ${e.toString()}');
rethrow;
}
}
@ -152,7 +152,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
},
onError: (error) {
isConnected = false;
Logger.error('连接失败: $error');
Logger.error('连接失败: ${error.toString()}');
},
onDone: () {
isConnected = false;
@ -161,7 +161,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
cancelOnError: true,
);
} catch (e) {
Logger.error("初始化异常----------------$e");
Logger.error("初始化异常----------------${e.toString()}");
}
}
@ -185,7 +185,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
}
}
}
Logger.info(
Logger.info(
"识别到的内容 type:${data['cn']['st']['type']} msg:$msg rl:$role");
if (msg.isNotEmpty) {
if (data['cn']['st']['type'] == "0") {
@ -206,7 +206,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
}
}
} catch (e) {
Logger.error("接受消息异常 $e");
Logger.error("接受消息异常 ${e.toString()}");
}
}
@ -242,7 +242,7 @@ class XunfeiAsrService extends GetxService implements AsrService {
_isContinuousRecognitionActive = false;
return true;
} catch (e) {
Logger.error('停止连续语音识别失败: $e');
Logger.error('停止连续语音识别失败: ${e.toString()}');
rethrow;
}
}
@ -266,8 +266,8 @@ class XunfeiAsrService extends GetxService implements AsrService {
Logger.info('Azure 语音识别资源已释放');
} catch (e) {
Logger.error('释放语音识别资源失败: $e');
Logger.error('释放语音识别资源失败: ${e.toString()}');
_isInitialized = false;
}
}
}
}

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

@ -76,7 +76,7 @@ class AndroidVoiceInteractionService extends GetxService {
_handleVoiceInteractionEvent(event);
}
}, onError: (error) {
Logger.error('语音交互事件通道错误: $error');
Logger.error('语音交互事件通道错误: ${error.toString()}');
});
}
@ -106,7 +106,7 @@ class AndroidVoiceInteractionService extends GetxService {
Logger.info('语音交互服务初始化完成');
return this;
} catch (e) {
Logger.error('语音交互服务初始化失败: $e');
Logger.error('语音交互服务初始化失败: ${e.toString()}');
return this;
}
}
@ -123,7 +123,7 @@ class AndroidVoiceInteractionService extends GetxService {
_isServiceRunning.value = isRunning;
Logger.info('语音交互服务状态: ${isRunning ? "运行中" : "未运行"}');
} catch (e) {
Logger.error('检查语音交互服务状态失败: $e');
Logger.error('检查语音交互服务状态失败: ${e.toString()}');
_isServiceRunning.value = false;
}
}
@ -151,7 +151,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result;
} catch (e) {
Logger.error('启动语音交互服务失败: $e');
Logger.error('启动语音交互服务失败: ${e.toString()}');
return false;
}
}
@ -175,7 +175,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result;
} catch (e) {
Logger.error('停止语音交互服务失败: $e');
Logger.error('停止语音交互服务失败: ${e.toString()}');
return false;
}
}
@ -199,7 +199,7 @@ class AndroidVoiceInteractionService extends GetxService {
return result;
} catch (e) {
Logger.error('暂停语音交互失败: $e');
Logger.error('暂停语音交互失败: ${e.toString()}');
return false;
}
}
@ -213,7 +213,7 @@ class AndroidVoiceInteractionService extends GetxService {
final int timestamp = eventMap['timestamp'] as int? ?? 0;
// 添加时间戳日志,帮助调试
Logger.info('收到原生端事件: $eventType, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}');
Logger.info('收到原生端事件: ${eventType.toString()}, 时间戳: $timestamp, 当前时间: ${DateTime.now().millisecondsSinceEpoch}');
switch (eventType) {
case 'chatHistory':
@ -252,7 +252,7 @@ class AndroidVoiceInteractionService extends GetxService {
break;
default:
Logger.warning('收到未知类型的语音交互事件: $eventType');
Logger.warning('收到未知类型的语音交互事件: ${eventType.toString()}');
break;
}
}
@ -297,7 +297,7 @@ class AndroidVoiceInteractionService extends GetxService {
// 保存更新后的历史记录
await chatHistoryService.saveHistory(agentId, existingMessages);
} catch (e) {
Logger.error('保存聊天记录失败: $e');
Logger.error('保存聊天记录失败: ${e.toString()}');
}
}

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

@ -66,7 +66,7 @@ class IosVoiceInteractionService extends GetxService {
// 保持idle状态,不自动启动语音识别
Logger.info('iOS语音交互服务初始化完成,处于idle状态');
} catch (e) {
Logger.error('iOS语音交互服务初始化失败: $e');
Logger.error('iOS语音交互服务初始化失败: ${e.toString()}');
}
return this;
@ -110,13 +110,13 @@ class IosVoiceInteractionService extends GetxService {
// 监听识别结果
_recognitionSubscription =
recognitionStream.listen(_handleRecognitionEvent, onError: (error) {
Logger.error('语音识别错误: $error');
Logger.error('语音识别错误: ${error.toString()}');
_startContinuousListening(); // 尝试重新启动
});
Logger.info('开始连续语音识别');
} catch (e) {
Logger.error('启动语音识别失败: $e');
Logger.error('启动语音识别失败: ${e.toString()}');
}
}
@ -263,7 +263,7 @@ class IosVoiceInteractionService extends GetxService {
// 处理完成
isProcessingAI.value = false;
} catch (e) {
Logger.error('AI响应处理失败: $e');
Logger.error('AI响应处理失败: ${e.toString()}');
// 错误恢复
isProcessingAI.value = false;
}
@ -437,7 +437,7 @@ class IosVoiceInteractionService extends GetxService {
Logger.info('保存聊天历史记录成功');
} catch (e) {
Logger.error('保存聊天历史记录失败: $e');
Logger.error('保存聊天历史记录失败: ${e.toString()}');
}
}
@ -463,4 +463,4 @@ class IosVoiceInteractionService extends GetxService {
_eventStreamController.add(event);
Logger.info('已发送识别开始事件');
}
}
}

4
lib/firebase_options.dart

@ -66,7 +66,7 @@ class DefaultFirebaseOptions {
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com',
iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com',
iosBundleId: 'com.example.client',
iosBundleId: 'com.yunqiinnovation.deepsound',
);
static const FirebaseOptions macos = FirebaseOptions(
@ -77,7 +77,7 @@ class DefaultFirebaseOptions {
storageBucket: 'deepsound-e0e3d.firebasestorage.app',
androidClientId: '907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com',
iosClientId: '907754492821-ej6j6pfto66jfhpj47b9bjs9amvo9dde.apps.googleusercontent.com',
iosBundleId: 'com.example.client',
iosBundleId: 'com.yunqiinnovation.deepsound',
);
static const FirebaseOptions windows = FirebaseOptions(

4
lib/main.dart

@ -24,8 +24,10 @@ void main() async {
// 初始化环境变量
await dotenv.load();
// 初始化登录服务(包含Firebase初始化)
LoginService();
//LoginService.FirebaseInit();
// 设置窗口大小(仅在 macOS 下生效)
if (Platform.isMacOS) {
const double width = 375.0;

314
lib/modules/agent/controllers/agent_controller.dart

@ -1,16 +1,20 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:amap_service/amap_service.dart';
import 'package:deep_voice/data/services/music_manager.dart';
import 'package:deep_voice/data/services/navigation_manager.dart';
import 'package:deep_voice/routes/app_routes.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:agent_service/agent_service.dart';
import 'package:logger/logger.dart';
import 'package:chat_storage/chat_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:get_storage/get_storage.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import 'package:deep_voice/core/utils/logger.dart';
import 'package:path_provider/path_provider.dart';
class Message {
final bool isUser; //标识信息是否来自用户还是AI
@ -36,8 +40,6 @@ class Message {
class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式
final logger = Logger();
// 聊天消息列表
final messages = <Message>[].obs;
@ -47,6 +49,7 @@ class AgentController extends GetxController {
//音乐播放管理
final _musiceManager = Get.find<MusicManager>();
final _navigationManager = Get.find<NavigationManager>();
var isPlaying = false.obs;
var currentUrl = "".obs;
@ -74,10 +77,15 @@ class AgentController extends GetxController {
// 流事件订阅
StreamSubscription<AgentServiceEvent>? _eventSubscription;
// 导航流事件订阅
StreamSubscription<AmapServiceEvent>? _amapeventSubscription;
// 用于跟踪AI回复状态
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
final TAG = 'AgentController';
// 会话ID
final String sessionId = 'default_agent';
@ -109,7 +117,7 @@ class AgentController extends GetxController {
isTtsEnabled.value = storage.read('agent_tts_enabled') ?? true;
} catch (e) {
// 如果读取失败,使用默认值
logger.e('加载TTS偏好设置失败: $e');
Logger.e(TAG, '加载TTS偏好设置失败: $e');
isTtsEnabled.value = true;
}
}
@ -128,18 +136,19 @@ class AgentController extends GetxController {
final storage = GetStorage();
await storage.write('agent_tts_enabled', isTtsEnabled.value);
} catch (e) {
logger.e('保存TTS偏好设置失败: $e');
Logger.e(TAG, '保存TTS偏好设置失败: $e');
}
}
// 加载聊天历史记录
Future<void> _loadChatHistory() async {
try {
logger.i('正在加载聊天历史...');
Logger.i(TAG, '正在加载聊天历史...');
//通过本地插件ChatStorage获取聊天记录
final result = await ChatStorage.getMessages(
sessionId: sessionId,
page: 1,
pageSize: 20,
sessionId: sessionId, //会话ID
page: 1, //从第一页开始
pageSize: 20, //每页20条记录
);
if (result.messages.isNotEmpty) {
@ -148,11 +157,22 @@ class AgentController extends GetxController {
final meta = msg.metadata != null && msg.metadata != ""
? jsonDecode(msg.metadata!)
: null; // 如果为空,使用空对象
// 从元数据中提取图片信息
final hasImage = meta != null && meta.containsKey('hasImage')
? meta['hasImage'] as bool
: false;
final imagePath = meta != null && meta.containsKey('imagePath')
? meta['imagePath'] as String?
: null;
return Message(
isUser: msg.sender == 'user',
text: msg.message,
meta: meta,
timestamp: DateTime.parse(msg.timestamp),
hasImage: hasImage,
imagePath: imagePath,
);
}).toList();
@ -160,14 +180,30 @@ class AgentController extends GetxController {
historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp));
messages.assignAll(historyMessages);
logger.i('成功加载 ${historyMessages.length} 条聊天记录');
Logger.i(TAG, '成功加载 ${historyMessages.length} 条聊天记录');
// 详细打印每条消息的内容和属性,便于调试
Logger.d(TAG, '===== 历史消息详情开始 =====');
for (int i = 0; i < historyMessages.length; i++) {
final msg = historyMessages[i];
Logger.d(
TAG,
'消息[$i]: {'
'isUser: ${msg.isUser}, '
'text: "${msg.text.length > 50 ? msg.text.substring(0, 50) + "..." : msg.text}", '
'time: ${msg.timestamp}, '
'hasImage: ${msg.hasImage}, '
'imagePath: ${msg.imagePath ?? "无"}, '
'meta: ${msg.meta != null ? "有" : "无"}'
'}');
}
Logger.d(TAG, '===== 历史消息详情结束 =====');
} else {
// 没有历史记录时添加欢迎消息
_addWelcomeMessage();
logger.i('没有历史记录,添加欢迎消息');
Logger.i(TAG, '没有历史记录,添加欢迎消息');
}
} catch (e) {
logger.e('加载聊天历史失败: $e');
Logger.e(TAG, '加载聊天历史失败: $e');
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
@ -180,11 +216,11 @@ class AgentController extends GetxController {
void _addWelcomeMessage() {
messages.add(Message(
isUser: false,
text: '你好!我是Emma,我能帮你:\n\n'
'• 回答问题和提供信息\n'
'• 实时语音翻译\n'
'• 推荐音乐和播客\n'
'• 语音备忘录和提醒\n'
text: '你好!我是**小言**,我能帮你:\n\n'
'• 答问题和提供信息\n\n'
'• 实时语音翻译\n\n'
'• 推荐音乐和播客\n\n'
'• 语音备忘录和提醒\n\n'
'• 控制耳机功能\n\n'
'有什么我可以帮你的吗?',
));
@ -272,12 +308,14 @@ class AgentController extends GetxController {
case AgentServiceEventType.ttsStarted:
isSpeaking.value = true;
Logger.i(TAG, 'TTS开始播放');
break;
case AgentServiceEventType.ttsCompleted:
case AgentServiceEventType.ttsStopped:
case AgentServiceEventType.ttsCanceled:
isSpeaking.value = false;
Logger.i(TAG, 'TTS停止播放');
break;
case AgentServiceEventType.imageProcessing:
@ -339,7 +377,7 @@ class AgentController extends GetxController {
messages.refresh();
}
logger.e('代理服务错误: ${event.data['message']}');
Logger.e(TAG, '代理服务错误: ${event.data['message']}');
break;
case AgentServiceEventType.autoStop:
@ -373,14 +411,14 @@ class AgentController extends GetxController {
case AgentServiceEventType.functionCall:
final functionName = event.data['name'] ?? '';
final arguments = event.data['arguments'] ?? '';
// logger.i('Fultter 开始调用 mcp: $functionName, $arguments');
// debugPrint('Fultter 开始调用 mcp: $functionName, $arguments');
break;
case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? '';
final functionCall = event.data['function_call'] ?? '';
logger.i('Flutter 结束调用 mcp: $result, $functionCall');
Logger.i(TAG, 'Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) {
final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"];
@ -388,17 +426,17 @@ final responseId = event.data['responseId'] ?? '';
if (meta != null && meta is String && meta.isNotEmpty) {
try {
metaResult = jsonDecode(meta);
metaResult = jsonDecode(meta) as Map<String, dynamic>;
} catch (e) {
print('解析meta数据失败: $e');
Logger.e(TAG, '解析meta数据失败: $e');
}
}
if(metaResult.isNotEmpty){
if (metaResult.isNotEmpty) {
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息
messages.add(Message(isUser: false, text: "", meta: metaResult));
messages
.add(Message(isUser: false, text: "", meta: metaResult));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
@ -413,6 +451,16 @@ final responseId = event.data['responseId'] ?? '';
);
}
}
if (metaResult.containsKey("card_navigation")) {
final start = metaResult["card_navigation"]["start"] as String;
final end = metaResult["card_navigation"]["end"] as String;
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
Future.delayed(Duration(milliseconds: 1500), () {
// Logger.i(TAG, 'liwei-------- 打开导航页面: $start $end ');
// 2秒后执行的代码
openNavigation(start, end, "");
});
}
}
}
break;
@ -420,9 +468,61 @@ final responseId = event.data['responseId'] ?? '';
break;
}
});
//后台导航事件同步
_amapeventSubscription = _navigationManager.events().listen((event) {
switch (event.type) {
case AmapServiceEventType.starte:
Logger.i(TAG, '导航通知 started!');
break;
case AmapServiceEventType.stope:
Logger.i(TAG, '导航通知 stoped!');
break;
case AmapServiceEventType.update:
Logger.i(TAG, '导航通知 update! $event');
Message? result;
try {
result = messages.lastWhere(
(msg) =>
!msg.isUser &&
msg.meta?.containsKey("card_navigation") == true,
);
} catch (e) {
result = null;
}
if (result != null) {
int index = messages.indexOf(result);
if (index != -1) {
// ✅ 创建 meta 的深拷贝
final newMeta = Map<String, dynamic>.from(result.meta ?? {});
newMeta["card_navigation"]["navigation"] = event.data;
// ✅ 创建新的 Message 替换原来的
messages[index] = Message(
isUser: result.isUser,
text: result.text,
meta: newMeta,
timestamp: result.timestamp,
);
messages.refresh();
Logger.i(
TAG, '更新导航卡片! ${newMeta["card_navigation"]["navigation"]}');
}
}
break;
case AmapServiceEventType.error:
Logger.i(TAG, '导航通知 error!');
break;
case AmapServiceEventType.unknown:
Logger.i(TAG, '导航通知 unknown!');
break;
}
});
}
// 发送文本消息
// 发送消息(包括所有支持的消息类型)
Future<void> sendTextMessage() async {
// 如果图片输入是活跃的,使用选中的图片和文本框的内容
if (isImageInputActive.value && selectedImagePath.value != null) {
@ -452,7 +552,7 @@ final responseId = event.data['responseId'] ?? '';
await AgentService.processTextInput(text,
speakResponse: isTtsEnabled.value);
} catch (e) {
logger.e('发送文本消息失败: $e');
Logger.e(TAG, '发送文本消息失败: $e');
isProcessing.value = false;
}
}
@ -460,10 +560,13 @@ final responseId = event.data['responseId'] ?? '';
// 发送图片消息
Future<void> sendImageMessage(String imagePath, {String? text}) async {
if (imagePath.isEmpty || !File(imagePath).existsSync()) {
logger.e('图片不存在: $imagePath');
Logger.e(TAG, '图片不存在: $imagePath');
return;
}
// 复制图片到应用永久存储目录
final permanentImagePath = await _copyImageToPermanentStorage(imagePath);
// 显示的文本,如果没有提供则使用默认值
final displayText = text?.isNotEmpty == true ? text! : '[图片]';
@ -472,7 +575,7 @@ final responseId = event.data['responseId'] ?? '';
isUser: true,
text: displayText,
hasImage: true,
imagePath: imagePath,
imagePath: permanentImagePath,
);
messages.add(message);
@ -487,19 +590,84 @@ final responseId = event.data['responseId'] ?? '';
// 标记为新的AI回复
_isNewAssistantResponse = true;
// 保存消息到持久化存储
//await _saveMessageToStorage(message);
// 调用Agent Service处理图片,考虑TTS状态
await AgentService.processImageInput(
imagePath,
permanentImagePath,
text: text ?? '',
speakResponse: isTtsEnabled.value,
);
} catch (e) {
logger.e('处理图片失败: $e');
Logger.e(TAG, '处理图片失败: $e');
isProcessing.value = false;
isImageProcessing.value = false;
}
}
// 将图片复制到应用的永久存储目录
Future<String> _copyImageToPermanentStorage(String sourcePath) async {
try {
final File sourceFile = File(sourcePath);
if (!await sourceFile.exists()) {
Logger.e(TAG, '源图片文件不存在: $sourcePath');
return sourcePath; // 如果源文件不存在,返回原路径
}
// 创建应用文档目录下的chatImages文件夹
final appDir = await getApplicationDocumentsDirectory();
final imagesDir = Directory('${appDir.path}/chatImages');
if (!await imagesDir.exists()) {
await imagesDir.create(recursive: true);
}
// 生成唯一文件名
final fileName =
'${DateTime.now().millisecondsSinceEpoch}_${sourcePath.split('/').last}';
final targetPath = '${imagesDir.path}/$fileName';
// 复制文件
await sourceFile.copy(targetPath);
Logger.i(TAG, '图片已复制到永久存储: $targetPath');
return targetPath;
} catch (e) {
Logger.e(TAG, '复制图片到永久存储失败: $e');
return sourcePath; // 出错时返回原路径
}
}
// 保存消息到持久化存储
// Future<void> _saveMessageToStorage(Message message) async {
// try {
// // 准备元数据,包含图片信息
// Map<String, dynamic> metadata = {};
// if (message.meta != null) {
// metadata = Map<String, dynamic>.from(message.meta!);
// }
// // 添加图片相关信息到元数据
// if (message.hasImage) {
// metadata['hasImage'] = true;
// metadata['imagePath'] = message.imagePath;
// }
// // 保存消息
// await ChatStorage.saveMessage(
// sessionId: sessionId,
// sender: message.isUser ? 'user' : 'assistant',
// message: message.text,
// metadata: metadata.isNotEmpty ? jsonEncode(metadata) : null,
// //timestamp: message.timestamp.toIso8601String(),
// );
// Logger.i(TAG, '消息已保存到持久化存储');
// } catch (e) {
// Logger.e(TAG, '保存消息到持久化存储失败: $e');
// }
// }
// 切换输入模式
void toggleInputMode() async {
// 如果当前在图片输入模式,先清除
@ -531,37 +699,72 @@ final responseId = event.data['responseId'] ?? '';
// 检查麦克风权限
Future<bool> _checkMicrophonePermission() async {
try {
PermissionStatus status = await Permission.microphone.status;
// 创建待请求权限列表
List<Permission> permissionsToRequest = [];
if (Platform.isAndroid) {
// Android 需要考虑SDK版本不同的权限要求
permissionsToRequest.add(Permission.microphone);
} else if (Platform.isIOS) {
// iOS 只需要麦克风权限
permissionsToRequest.add(Permission.microphone);
}
// 批量请求所有需要的权限
final statuses = await permissionsToRequest.request();
// 如果已授权,直接返回 true
if (status.isGranted) {
// 检查是否所有权限都已授予
bool allGranted = true;
Permission? deniedPermission;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
deniedPermission = permission;
Logger.e(TAG, '权限 ${permission.toString()} 未被授予: $status');
}
});
// 单独检查麦克风权限状态,因为这是主要需求
final micStatus = await Permission.microphone.status;
// 如果所有权限都已授予,或至少麦克风权限已授予,返回true
if (allGranted || micStatus.isGranted) {
return true;
}
// 如果被永久拒绝,提示用户前往设置页面
if (status.isPermanentlyDenied) {
// 处理特定权限被永久拒绝的情况
if (micStatus.isPermanentlyDenied) {
_showPermissionDialog(true);
return false;
}
// 如果之前拒绝但不是永久拒绝,或首次请求,尝试请求权限
if (status.isDenied) {
status = await Permission.microphone.request();
if (status.isGranted) {
return true;
} else {
_showPermissionDialog(false);
return false;
}
// 普通拒绝情况
if (deniedPermission != null) {
_showPermissionDialog(false);
}
return false;
} catch (e) {
logger.e('检查麦克风权限失败: $e');
Logger.e(TAG, '检查麦克风权限失败: $e');
return false;
}
}
// 获取Android SDK版本
Future<int> _getAndroidSdkVersion() async {
if (Platform.isAndroid) {
try {
// 这里我们简单返回一个较高的值,确保请求所有可能需要的权限
// 在实际应用中,你可能需要使用platform channel获取真实的SDK版本
return 31; // 对应Android 12
} catch (e) {
Logger.e(TAG, '获取Android SDK版本失败: $e');
}
}
return 0;
}
// 显示权限提示对话框
void _showPermissionDialog(bool isPermanent) {
Get.dialog(
@ -605,7 +808,7 @@ final responseId = event.data['responseId'] ?? '';
try {
await AgentService.startConversation();
} catch (e) {
logger.e('启动语音输入失败: $e');
Logger.e(TAG, '启动语音输入失败: $e');
}
}
@ -616,7 +819,7 @@ final responseId = event.data['responseId'] ?? '';
try {
await AgentService.stopConversation();
} catch (e) {
logger.e('停止语音输入失败: $e');
Logger.e(TAG, '停止语音输入失败: $e');
}
}
@ -634,7 +837,7 @@ final responseId = event.data['responseId'] ?? '';
// 重置回复状态
_isNewAssistantResponse = true;
} catch (e) {
logger.e('清除聊天历史失败: $e');
Logger.e(TAG, '清除聊天历史失败: $e');
}
}
@ -656,7 +859,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true;
}
} catch (e) {
logger.e('选择图片失败: $e');
Logger.e(TAG, '选择图片失败: $e');
}
}
@ -678,7 +881,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true;
}
} catch (e) {
logger.e('拍照失败: $e');
Logger.e(TAG, '拍照失败: $e');
}
}
@ -720,7 +923,7 @@ final responseId = event.data['responseId'] ?? '';
// 播放音乐
void musicStatus() async {
var status = await _musiceManager.getStatus();
// logger.i('获取当前音乐播放状态 $status');
// debugPrint('获取当前音乐播放状态 $status');
isPlaying.value = status['isPlaying'];
currentUrl.value = status['url'];
}
@ -751,4 +954,9 @@ final responseId = event.data['responseId'] ?? '';
AgentService.processTextInput(messages[index].text,
speakResponse: isTtsEnabled.value);
}
//去导航
void openNavigation(String start, String end, String ntype) {
_navigationManager.open(start, end, ntype);
}
}

86
lib/modules/agent/views/agent_view.dart

@ -52,7 +52,7 @@ class AgentView extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Emma',
'小言'.tr,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@ -149,28 +149,32 @@ class AgentView extends StatelessWidget {
// 聊天内容
Expanded(
child: Obx(
() => ListView.builder(
controller: controller.scrollController,
reverse: true,
padding:
const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller
.messages[controller.messages.length - 1 - index];
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
mate: message.meta,
),
);
},
() => Align(
alignment: Alignment.topCenter,
child: ListView.builder(
controller: controller.scrollController,
shrinkWrap: true,
reverse: true,
padding:
const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
itemCount: controller.messages.length,
itemBuilder: (context, index) {
final message = controller
.messages[controller.messages.length - 1 - index];
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MessageBubble(
isUser: message.isUser,
message: message.text,
timestamp: message.timestamp,
isRecognizing: message.isRecognizing,
hasImage: message.hasImage,
imagePath: message.imagePath,
meta: message.meta,
),
);
},
),
),
),
),
@ -286,12 +290,16 @@ class AgentView extends StatelessWidget {
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[200],
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
),
child: Icon(
isTextMode ? Icons.mic_none : Icons.keyboard,
color: Colors.grey[700],
color: isDarkMode
? Colors.grey[300]
: Colors.grey[700],
size: 22,
),
),
@ -308,13 +316,18 @@ class AgentView extends StatelessWidget {
if (isTextMode) {
// 文本输入模式 - 注意观察图片模式时的高度变化
return Container(
height: 44,
//height: 44,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.withOpacity(0.1),
color: isDarkMode
? Colors.grey[
800]! // 修改: 从 Colors.grey[700] 改为 Colors.grey[800]
: Colors.grey.withOpacity(0.1),
width: 1.0,
),
),
@ -334,7 +347,7 @@ class AgentView extends StatelessWidget {
fontSize: 15,
color: isDarkMode
? Colors.white
: Colors.black87,
: Colors.grey[800],
),
decoration: InputDecoration(
hintText: isImageActive
@ -378,14 +391,18 @@ class AgentView extends StatelessWidget {
}
},
child: Container(
height: 44,
height: 50,
padding:
const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.grey[200],
color: isDarkMode
? Colors.grey[800]
: Colors.grey[200],
borderRadius: BorderRadius.circular(22),
border: Border.all(
color: Colors.grey.withOpacity(0.1),
color: isDarkMode
? Colors.grey[800]!
: Colors.grey.withOpacity(0.1),
width: 1.0,
),
),
@ -446,13 +463,14 @@ class AgentView extends StatelessWidget {
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.grey[200],
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(22),
),
child: PopupMenuButton<String>(
icon: Icon(
Icons.add_photo_alternate_outlined,
color: Colors.grey[700],
color:
isDarkMode ? Colors.grey[300] : Colors.grey[700],
size: 22,
),
padding: EdgeInsets.zero,

1061
lib/modules/agent/views/message_bubble.dart

File diff suppressed because it is too large

261
lib/modules/agent/views/music_card.dart

@ -0,0 +1,261 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
/// 构建音乐项目
Widget buildMusicItem(BuildContext context, String id, String name, String url,
String singer, String image) {
final controller = Get.find<AgentController>(); // 获取控制器
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
Map<String, String> song = {
'id': id,
'url': url,
'title': name,
'artist': singer,
'coverUrl': image,
};
controller.musicStatus(); //刷新状态
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: isDarkMode ? Color(0xFF1E1E1E) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.2 : 0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
image: image.isNotEmpty
? DecorationImage(
image: NetworkImage(image),
fit: BoxFit.cover,
)
: null,
),
child: image.isEmpty
? Icon(
Icons.music_note,
size: 18,
color: Theme.of(context).primaryColor,
)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Text(
singer,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
],
),
),
Obx(() {
final isThisPlaying =
controller.isPlaying.value && controller.currentUrl.value == url;
return IconButton(
onPressed: () {
if (controller.isPlaying.value) {
if (controller.currentUrl.value == url) {
controller.musicPause();
} else {
controller.musicPlay(song);
}
} else {
if (controller.currentUrl.value == url) {
controller.musicResume();
} else {
controller.musicPlay(song);
}
}
},
icon: Icon(
isThisPlaying ? Icons.pause_circle : Icons.play_circle,
color: Theme.of(context).primaryColor,
),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 36,
);
}),
],
),
);
}
/// 构建音乐内容卡片
Widget buildMusicContent(
BuildContext context, List<Map<String, dynamic>> playlist) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'以下是一些推荐的音乐:',
style: TextStyle(
color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
// Container(
// padding: const EdgeInsets.all(12),
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(12),
// boxShadow: [
// BoxShadow(
// color: Colors.black.withOpacity(0.03),
// blurRadius: 3,
// spreadRadius: 1,
// offset: const Offset(0, 1),
// ),
// ],
// ),
// child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.music_note,
size: 16,
color: Theme.of(context).primaryColor.withOpacity(0.7),
),
const SizedBox(width: 6),
Text(
'播放列表'.tr,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[300]
: Colors.grey[700],
),
),
],
),
const SizedBox(height: 10),
// 动态生成音乐条目
...playlist.asMap().entries.take(4).map((entry) {
final index = entry.key;
final item = entry.value;
final id = item["id"]?.toString() ?? "";
final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";
final image = item["image"] ?? "";
return Column(
children: [
buildMusicItem(context, id, name, url, singer, image),
if (index != playlist.length - 1)
Divider(
height: 1,
thickness: 1,
color: Theme.of(context).brightness == Brightness.dark
? Colors.grey[800]
: Colors.grey[100],
),
],
);
}).toList(),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多推荐',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
//),
//const SizedBox(height: 10),
// Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Expanded(
// child: OutlinedButton.icon(
// onPressed: () {},
// icon: Icon(Icons.play_arrow, size: 16),
// label: Text('播放全部'),
// style: OutlinedButton.styleFrom(
// foregroundColor: Theme.of(context).primaryColor,
// side: BorderSide(
// color: Theme.of(context).primaryColor.withOpacity(0.5)),
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// textStyle: TextStyle(fontSize: 12),
// ),
// ),
// ),
// SizedBox(width: 8),
// Expanded(
// child: OutlinedButton.icon(
// onPressed: () {},
// icon: Icon(Icons.shuffle, size: 16),
// label: Text('随机播放'),
// style: OutlinedButton.styleFrom(
// foregroundColor: Colors.grey[700],
// side: BorderSide(color: Colors.grey[400]!),
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
// shape: RoundedRectangleBorder(
// borderRadius: BorderRadius.circular(16),
// ),
// textStyle: TextStyle(fontSize: 12),
// ),
// ),
// ),
// ],
// ),
],
);
}

157
lib/modules/agent/views/navigation_card.dart

@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/agent_controller.dart';
String formatDistance(String distanceStr) {
final distance = double.tryParse(distanceStr);
if (distance == null) return distanceStr; // 如果无法解析,则返回原始字符串
if (distance >= 1000) {
final km = distance / 1000;
return '${km.toStringAsFixed(1)} 公里';
} else {
return '${distance.toStringAsFixed(0)} 米';
}
}
String formatDuration(String secondsStr) {
int totalSeconds = int.tryParse(secondsStr) ?? 0;
Duration duration = Duration(seconds: totalSeconds);
int hours = duration.inHours;
int minutes = duration.inMinutes.remainder(60);
int seconds = duration.inSeconds.remainder(60);
List<String> parts = [];
if (hours > 0) parts.add('$hours小时');
if (minutes > 0) parts.add('$minutes分钟');
if (seconds > 0 || parts.isEmpty) parts.add('$seconds秒');
return parts.join('');
}
//导航卡片
Widget navigationCard(
BuildContext context, {
required String start,
required String end,
required String destination,
required List<Map<String, dynamic>> modes,
Map<String, dynamic>? navigation,
double height = 300,
}) {
int currentIndex = 0;
final controller = Get.find<AgentController>(); // 获取控制器
return Container(
height: height,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.white,
boxShadow: const [
BoxShadow(
blurRadius: 8,
color: Colors.black12,
offset: Offset(0, 4),
),
],
),
child: StatefulBuilder(
builder: (context, setInnerState) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 路线图片
ClipRRect(
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Image.network(
modes[currentIndex]["image"] as String,
key: ValueKey<String>(modes[currentIndex]["ntype"] as String),
height: 160,
width: double.infinity,
fit: BoxFit.cover,
),
),
),
// 导航方式切换按钮
Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(modes.length, (index) {
final isSelected = index == currentIndex;
return GestureDetector(
onTap: modes[index]['enable'] as bool
? () {
setInnerState(() => currentIndex = index);
}
: null,
child: SizedBox(
width: 50,
child: Text(
(modes[index]["ntype"] as String).tr,
textAlign: TextAlign.center,
style: TextStyle(
color: modes[index]['enable']
? isSelected
? Colors.blue
: Colors.black87
: Colors.grey,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
);
}),
),
),
// 距离和耗时
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('$destination',
style: const TextStyle(
fontSize: 12, color: Colors.black)),
Text(
'${formatDistance(modes[currentIndex]["distance"])}-约${formatDuration(modes[currentIndex]["duration"])}',
style: const TextStyle(
fontSize: 15, color: Colors.blue)),
if (navigation != null)
Text(
"导航中... ${navigation["distanceText"]}进入${navigation["nextRoad"]}",
style: TextStyle(
fontSize: 12,
color:
const Color.fromARGB(255, 164, 164, 164)))
],
),
CircleAvatar(
backgroundColor: Colors.blue,
radius: 18,
child: IconButton(
icon: const Icon(Icons.turn_left,
color: Colors.white, size: 20),
onPressed: () {
controller.openNavigation(start, end, "");
},
),
)
],
),
),
],
);
},
),
);
}

211
lib/modules/agent/views/travel_card.dart

@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
/// 构建旅游内容卡片
Widget buildTravelContent(
BuildContext context, List<Map<String, dynamic>> travellist) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'以下是一些推搜索结果:',
style: TextStyle(
color: Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 3,
spreadRadius: 1,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Row(
// children: [
// Icon(
// Icons.place,
// size: 16,
// color: Color(0xFFE11D48),
// ),
// const SizedBox(width: 6),
// Text(
// '热门景点 · 东京',
// style: TextStyle(
// fontSize: 13,
// fontWeight: FontWeight.w600,
// color: Colors.grey[700],
// ),
// ),
// ],
// ),
// const SizedBox(height: 10),
// 动态生成音乐条目
...travellist.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final name = item["name"] ?? "未知歌曲"; //名字
final addr = item["addr"] ?? ""; //地址
final tag = item["tag"] ?? "未知歌手"; //标签
final rating = item["rating"] ?? ""; //评分
final distance = item["distance"] ?? ""; //距离
final images = item["images"] ?? []; // 图片列表
return Column(
children: [
buildLocationItem(
context,
name,
addr,
),
if (index != travellist.length - 1)
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
],
);
}).toList(),
// // 景点列表
// _buildLocationItem('东京塔', '标志性建筑,可俯瞰东京全景', context),
// Divider(height: 1, thickness: 1, color: Colors.grey[100]),
// _buildLocationItem('涩谷十字路口', '世界上最繁忙的十字路口之一', context),
// Divider(height: 1, thickness: 1, color: Colors.grey[100]),
// _buildLocationItem('浅草寺', '东京最古老的寺庙', context),
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: () {},
style: TextButton.styleFrom(
minimumSize: Size.zero,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
'查看更多景点',
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.map, size: 16),
label: Text('查看地图'),
style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(context).primaryColor,
side: BorderSide(
color: Theme.of(context).primaryColor.withOpacity(0.5)),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () {},
icon: Icon(Icons.bookmark_border, size: 16),
label: Text('保存到收藏'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.grey[700],
side: BorderSide(color: Colors.grey[400]!),
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
textStyle: TextStyle(fontSize: 12),
),
),
),
],
),
],
);
}
/// 构建地点项目
Widget buildLocationItem(
BuildContext context, String name, String description) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Container(
width: 38,
height: 38,
decoration: BoxDecoration(
color: isDarkMode ? Color(0xFF1A365D) : Colors.blue[50],
borderRadius: BorderRadius.circular(8),
),
child: Icon(
Icons.place,
size: 18,
color: Color(0xFFE11D48),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: isDarkMode
? Colors.white.withOpacity(0.9)
: Colors.grey[800],
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Text(
description,
style: TextStyle(
fontSize: 12,
color: isDarkMode ? Colors.grey[400] : Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
],
),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.arrow_forward, color: Colors.grey[400]),
padding: EdgeInsets.zero,
constraints: BoxConstraints(),
iconSize: 20,
),
],
),
);
}

313
lib/modules/agent/views/weather_card.dart

@ -0,0 +1,313 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
/// 构建天气内容卡片
Widget buildWeatherContent(BuildContext context, String city,
Map<String, dynamic> now, List<Map<String, dynamic>> days) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'查询天气情况:',
style: TextStyle(
color: isDarkMode ? Colors.white.withOpacity(0.9) : Colors.black87,
fontSize: 14,
height: 1.4,
fontWeight: FontWeight.w400,
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: isDarkMode
? LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF1E40AF), Color(0xFF1E3A8A)],
)
: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF60A5FA), Color(0xFF3B82F6)],
),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${city} ${now["date"]}",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(height: 4),
Text(
now["weatherCondition"],
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
),
SizedBox(height: 8),
Row(
children: [
Text(
"${now["temperature"]}°",
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最高: ${now["maxTemperature"]}°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
Text(
'最低: ${now["minTemperature"]}°',
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
],
),
],
),
],
),
const Icon(
Icons.wb_sunny,
size: 48,
color: Colors.white,
),
],
),
SizedBox(height: 16),
Container(
height: 1,
color: Colors.white.withOpacity(isDarkMode ? 0.15 : 0.2),
),
SizedBox(height: 16),
const Text(
'未来预报',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
days.length,
(index) {
String dateStr = days[index]['date'];
DateTime? date = DateUtil.getDateTime(dateStr);
IconData weatherIcon =
getWeatherIcon(days[index]['weatherCondition']);
String weekday = DateUtil.getWeekday(date,
languageCode: "zh", short: true);
String temp = "${days[index]['temperature']}°";
return _buildForecastDay(
weekday,
weatherIcon,
temp,
);
},
),
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.15)
: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Icon(
Icons.info_outline,
size: 16,
color: Colors.white,
),
SizedBox(width: 8),
Flexible(
child: Text(
'今日紫外线指数较高,外出请做好防晒',
style: TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
],
),
),
],
),
),
],
);
}
/// 构建单日天气预报
Widget _buildForecastDay(String day, IconData icon, String temp) {
return Column(
children: [
Text(
day,
style: TextStyle(
fontSize: 12,
color: Colors.white.withOpacity(0.9),
),
),
SizedBox(height: 4),
Icon(
icon,
size: 20,
color: Colors.white,
),
SizedBox(height: 4),
Text(
temp,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
],
);
}
/// 获取天气图标
IconData getWeatherIcon(String description) {
description = description.trim();
if (["晴", "少云", "晴间多云"].contains(description)) {
return Icons.wb_sunny;
}
if (["多云", "阴"].contains(description)) {
return Icons.cloud;
}
if (description.contains("雾") ||
["雾", "浓雾", "强浓雾", "轻雾", "大雾", "特强浓雾", "霾", "中度霾", "重度霾", "严重霾"]
.contains(description)) {
return Icons.blur_on;
}
if (description.contains("风") ||
[
"有风",
"平静",
"微风",
"和风",
"清风",
"强风/劲风",
"疾风",
"大风",
"烈风",
"风暴",
"狂爆风",
"飓风",
"热带风暴"
].contains(description)) {
return Icons.air;
}
if (description.contains("雨") ||
[
"阵雨",
"雷阵雨",
"雷阵雨并伴有冰雹",
"小雨",
"中雨",
"大雨",
"暴雨",
"大暴雨",
"特大暴雨",
"强阵雨",
"强雷阵雨",
"极端降雨",
"毛毛雨/细雨",
"雨",
"小雨-中雨",
"中雨-大雨",
"大雨-暴雨",
"暴雨-大暴雨",
"大暴雨-特大暴雨"
].contains(description)) {
return Icons.grain; // 或使用 community icon 包:weather_icons 中的 rain
}
if (description.contains("雪") ||
[
"雨雪天气",
"雨夹雪",
"阵雨夹雪",
"冻雨",
"雪",
"阵雪",
"小雪",
"中雪",
"大雪",
"暴雪",
"小雪-中雪",
"中雪-大雪",
"大雪-暴雪"
].contains(description)) {
return Icons.ac_unit;
}
if (["浮尘", "扬沙", "沙尘暴", "强沙尘暴"].contains(description)) {
return Icons.filter_drama;
}
if (["龙卷风"].contains(description)) {
return Icons.waves;
}
if (["热"].contains(description)) {
return Icons.local_fire_department;
}
if (["冷"].contains(description)) {
return Icons.ac_unit;
}
// 默认或未知天气
return Icons.help_outline;
}

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

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:amap_flutter/amap_flutter.dart';
import '../../../data/services/spotify_service.dart';
import 'package:get/get.dart';
import 'package:flutter/widgets.dart';
@ -8,7 +7,6 @@ import 'package:get_storage/get_storage.dart';
import 'package:haptic_feedback/haptic_feedback.dart';
import 'package:just_audio/just_audio.dart';
import 'package:chat_storage/chat_storage.dart';
import 'package:path/path.dart';
import '../../../data/services/ai_service.dart';
import '../../../data/services/deapsound_ai_service.dart';
import '../../../data/services/kouzi_ai_service.dart';
@ -32,7 +30,6 @@ class ChatController extends GetxController {
// 添加ScrollController用于控制列表滚动
final ScrollController scrollController = ScrollController();
final _amapFlutter = AmapFlutter();
StreamSubscription? _locationStream;
String _latLng = '';
String _city = '深圳市';
@ -187,7 +184,6 @@ class ChatController extends GetxController {
// 自动停止计时器会在startVoiceRecognition中启动
}
_getLocation();
player.playerStateStream.listen((state) async {
if (state.processingState == ProcessingState.completed) {
@ -203,15 +199,6 @@ class ChatController extends GetxController {
});
}
void _getLocation() async {
await _amapFlutter.initLocation();
_locationStream = _amapFlutter.locationStream().listen((data) async {
_latLng = '${data['longitude']},${data['latitude']}}';
_city = data['city'];
_amapFlutter.stopLocation();
});
_amapFlutter.startLocation();
}
@override
void onClose() {
@ -228,7 +215,6 @@ class ChatController extends GetxController {
_talkDebounceTimer?.cancel();
_talkStartDebounceTimer?.cancel();
_amapFlutter.dispose();
_locationStream?.cancel();
// 停止TTS
@ -451,7 +437,7 @@ class ChatController extends GetxController {
}
isPlay.value = true;
} catch (e) {
Logger.error('加载异常: $e');
Logger.error('加载异常: ${e.toString()}');
}
}
@ -469,7 +455,7 @@ class ChatController extends GetxController {
isPlay.value = true;
audiourl.value = card['url'] as String;
} catch (e) {
Logger.error('加载异常: $e');
Logger.error('加载异常: ${e.toString()}');
}
}
@ -479,7 +465,7 @@ class ChatController extends GetxController {
_spotifService.pause();
isPlay.value = false;
} catch (e) {
Logger.error('加载异常: $e');
Logger.error('加载异常: ${e.toString()}');
}
}
@ -872,7 +858,7 @@ class ChatController extends GetxController {
}
// 删除数据库中的历史记录
ChatStorage.deleteMessages(sessionId: agent.id).catchError((e) {
Logger.error('清空数据库聊天记录失败: $e');
Logger.error('清空数据库聊天记录失败: ${e.toString()}');
});
}
@ -1003,4 +989,4 @@ class ChatController extends GetxController {
.abs() <
5000);
}
}
}

14
lib/modules/common/webview_page.dart

@ -23,13 +23,13 @@ class WebViewPage extends StatelessWidget {
centerTitle: true,
title: Text(title),
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
icon: Icon(
Icons.arrow_back_ios,
color: isDarkMode ? Colors.white : Colors.black,
size: 20.sp,
),
onPressed: () => Get.back(),
),
onPressed: () => Get.back(),
),
backgroundColor: isDarkMode ? Colors.black : Colors.white,
foregroundColor: isDarkMode ? Colors.white : Colors.black,
elevation: 0,
@ -39,4 +39,4 @@ class WebViewPage extends StatelessWidget {
),
);
}
}
}

3
lib/modules/home/controllers/home_controller.dart

@ -103,6 +103,9 @@ class HomeController extends GetxController {
// 设置监听器监听蓝牙服务状态变化
_setupStateListeners();
// 在初始化时自动触发一次刷新设备连接状态
refreshDeviceConnection();
}
/// 设置状态监听器

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

@ -1,11 +0,0 @@
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

@ -1,323 +0,0 @@
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

@ -1,568 +0,0 @@
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;
}
}
}

69
lib/modules/meeting/controllers/meeting_details_controller.dart

@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:audio_waveforms/audio_waveforms.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:http/http.dart' as http;
@ -18,13 +19,18 @@ import '../model/meeting_model.dart';
class MeetingDetailsController extends GetxController
with GetSingleTickerProviderStateMixin {
late ScrollController scrollController;
late TabController tabController;
final RxInt currentIndex = 0.obs;
final PlayerController playerController = PlayerController();
RxBool isTop = false.obs;
RxInt durationMax = 0.obs;
RxDouble dragOffset = 210.w.obs;
RxInt currentDuration = 0.obs;
RxInt speakerAllStarttime = (-1).obs;
RxInt speakerAllEndtime = (-1).obs;
int id = 0;
@ -52,10 +58,11 @@ class MeetingDetailsController extends GetxController
@override
void onInit() {
super.onInit();
scrollController = ScrollController(initialScrollOffset: 0.0);
scrollController.addListener(_onScroll);
tabController = TabController(length: 4, vsync: this);
tabController.addListener(() {
if (currentIndex.value == 2 && tabController.index != 2) {
FocusScope.of(Get.context!).unfocus();
}
currentIndex.value = tabController.index;
});
final Map<String, dynamic> args = Get.arguments ?? {};
@ -70,55 +77,43 @@ class MeetingDetailsController extends GetxController
@override
void onClose() {
scrollController.dispose();
tabController.dispose();
playerController.dispose();
_isDisposed = true;
super.onClose();
}
void _onScroll() async {
double pixels = scrollController.position.pixels;
double maxScrollExtent = scrollController.position.maxScrollExtent;
if (!isTop.value && pixels > 5) {
await scrollController.animateTo(
maxScrollExtent,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
);
if (pixels > maxScrollExtent - 2) {
isTop.value = true;
}
}
if (isTop.value && pixels < maxScrollExtent - 5) {
await scrollController.animateTo(
0,
duration: const Duration(milliseconds: 100),
curve: Curves.easeOut,
);
if (pixels < 2) {
isTop.value = false;
}
}
}
void _getAgent() async {
final GetStorage storage = GetStorage();
Map userInfo = storage.read("user_info");
_userId = userInfo['user']['uid'];
_systemPrompt = AppConfig.agentsystemPrompt("personal_assistant") ?? '';
_systemPrompt = AppConfig.agentsystemPrompt("meet_assistant") ?? '';
}
void _getDataDetails() async {
final data = await SqfliteApi.getMeetingDetails(id);
meetingData.value = MeetingModel.fromJson(data);
meetingDetails.value = MeetingDetailsModel.fromJson(data['details']);
if (meetingDetails.value.summary.isNotEmpty) {
dragOffset.value = 0;
isTop.value = true;
}
await playerController.preparePlayer(
path: meetingData.value.filepath,
noOfSamples: 1200,
);
durationMax.value = await playerController.getDuration(DurationType.max);
playerController.setFinishMode(finishMode: FinishMode.pause);
playerController.onCurrentDurationChanged.listen((int position) {
currentDuration.value = position;
if (speakerAllEndtime.value > 0) {
if (position >= speakerAllEndtime.value) {
playerController.pausePlayer();
speakerAllStarttime.value = -1;
speakerAllEndtime.value = -1;
}
}
});
if (meetingDetails.value.tasktype == 1) {
List speakerList = await SqfliteApi.getMeetingSpeaker(id);
textList.value = List.from(speakerList);
@ -181,7 +176,7 @@ class MeetingDetailsController extends GetxController
String text = '';
for (var element in textList) {
if (element['speaker'].isNotEmpty) {
text += '${element['speaker']}:';
text += '[${element['speaker']}]:';
}
text += '${element['content']}\n';
}
@ -258,7 +253,7 @@ class MeetingDetailsController extends GetxController
Logger.debug('taskId: $taskId');
return taskId;
} catch (e) {
Logger.error('Submit error: $e');
Logger.error('Submit error: ${e.toString()}');
rethrow;
}
}
@ -290,7 +285,7 @@ class MeetingDetailsController extends GetxController
final respDic = jsonDecode(response.body);
return respDic;
} catch (e) {
Logger.error('Query error: $e');
Logger.error('Query error: ${e.toString()}');
rethrow;
}
}
@ -322,7 +317,7 @@ class MeetingDetailsController extends GetxController
}
}
} catch (e) {
Logger.error('Error occurred: $e');
Logger.error('Error occurred: ${e.toString()}');
}
}
@ -334,8 +329,10 @@ class MeetingDetailsController extends GetxController
String speaker = '';
if (element['additions'].isNotEmpty) {
speaker = 'Speaker ${element['additions']['speaker']}';
text += '$speaker:';
personnel += '[$speaker] ';
text += '[$speaker]:';
if (!personnel.contains(speaker)) {
personnel += '[$speaker] ';
}
}
list.add({
'meetingid': id,

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

@ -22,139 +22,7 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
() => Scaffold(
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50],
appBar: _buildAppBar(isDarkMode),
body: NestedScrollView(
controller: controller.scrollController,
headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
return [
SliverAppBar(
automaticallyImplyLeading: false,
expandedHeight: 210.w,
backgroundColor: Colors.transparent,
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.pin,
background: Align(
alignment: Alignment.topCenter,
child: Container(
height: 200.w,
decoration: BoxDecoration(
color: Colors.grey[200]!,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(20.r),
),
),
child: Column(
children: [
Container(
margin: EdgeInsets.only(bottom: 5.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
),
child: AudioFileWaveforms(
size: Size(350.w, 120.w),
playerController: controller.playerController,
playerWaveStyle: PlayerWaveStyle(
fixedWaveColor: Colors.grey[300]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 600,
),
),
),
SizedBox(
width: 350.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
StreamBuilder<int>(
stream: controller.playerController
.onCurrentDurationChanged,
builder: (context, snapshot) {
final position = snapshot.data ?? 0;
return Text(
DateUtil.formatDateMs(
position,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
),
);
},
),
Obx(
() => Text(
DateUtil.formatDateMs(
controller.durationMax.value,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
),
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(
Icons.replay_10,
color: Colors.black,
size: 30.w,
),
StreamBuilder<PlayerState>(
stream: controller
.playerController.onPlayerStateChanged,
builder: (context, snapshot) {
bool isPlay =
snapshot.data?.isPlaying ?? false;
return GestureDetector(
onTap: () {
if (isPlay) {
controller.playerController
.pausePlayer();
} else {
controller.playerController
.startPlayer();
}
},
child: Icon(
isPlay
? Icons.pause_circle
: Icons.play_circle,
color: Colors.black,
size: 48.w,
),
);
},
),
Icon(
Icons.forward_10,
color: Colors.black,
size: 30.w,
),
],
),
],
),
),
),
),
),
];
},
body: _buildBody(context, isDarkMode),
),
body: _buildBody(context, isDarkMode),
),
);
}
@ -224,14 +92,22 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
},
),
4.horizontalSpace,
AudioFileWaveforms(
size: Size(130.w, 30.w),
playerController: controller.playerController,
playerWaveStyle: PlayerWaveStyle(
fixedWaveColor: Colors.grey[400]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 100,
GestureDetector(
onTap: () {
if (controller.isSpeakerText.value) return;
controller.dragOffset.value = 210.w;
controller.isTop.value = false;
},
child: AudioFileWaveforms(
size: Size(130.w, 30.w),
playerController: controller.playerController,
enableSeekGesture: false,
playerWaveStyle: PlayerWaveStyle(
fixedWaveColor: Colors.grey[400]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 100,
),
),
),
],
@ -293,20 +169,67 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
// 构建主体内容
Widget _buildBody(BuildContext context, bool isDarkMode) {
double initialPosition = 0;
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w),
margin: EdgeInsets.symmetric(vertical: 10.w),
child: Obx(
() => Row(
children: [
_iconItem(Icons.summarize, '概述', 0),
_iconItem(Icons.message, '转写', 1),
_iconItem(Icons.article, '总结', 2),
_iconItem(Icons.lan, '思维导图', 3),
],
),
GestureDetector(
behavior: HitTestBehavior.translucent,
onVerticalDragStart: (DragStartDetails dragStartDetails) {
initialPosition = dragStartDetails.localPosition.dy;
},
onVerticalDragUpdate: (DragUpdateDetails dragUpdateDetails) {
controller.dragOffset.value += dragUpdateDetails.delta.dy;
},
onVerticalDragEnd: (DragEndDetails dragEndDetails) {
double offset = initialPosition - dragEndDetails.localPosition.dy;
if (offset > 0) {
if (controller.isTop.value) {
controller.dragOffset.value = 0;
return;
}
if (offset > 60) {
controller.dragOffset.value = 0;
controller.isTop.value = true;
} else {
controller.dragOffset.value = 210.w;
controller.isTop.value = false;
}
} else {
if (!controller.isTop.value) {
controller.dragOffset.value = 210.w;
return;
}
if (offset < -60) {
controller.dragOffset.value = 210.w;
controller.isTop.value = false;
} else {
controller.dragOffset.value = 0;
controller.isTop.value = true;
}
}
},
child: Column(
children: [
_soundWave(isDarkMode),
Obx(
() => !controller.isSpeakerText.value
? Container(
padding: EdgeInsets.symmetric(horizontal: 8.w),
margin: EdgeInsets.symmetric(vertical: 10.w),
child: Obx(
() => Row(
children: [
_iconItem(Icons.summarize, '概述', 0),
_iconItem(Icons.message, '转写', 1),
_iconItem(Icons.article, '总结', 2),
_iconItem(Icons.lan, '思维导图', 3),
],
),
),
)
: const SizedBox(),
),
],
),
),
Expanded(
@ -315,11 +238,15 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
child: TabBarView(
controller: controller.tabController,
physics: const NeverScrollableScrollPhysics(),
children: const [
OverviewTab(),
SpeechTab(),
SummaryTab(),
MindMapTab(),
children: [
const OverviewTab(),
const SpeechTab(),
const SummaryTab(),
Obx(
() => controller.meetingDetails.value.summary.isNotEmpty
? const MindMapTab()
: const SizedBox(),
),
],
),
),
@ -328,6 +255,148 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
);
}
Widget _soundWave(bool isDarkMode) {
return Obx(
() => SizedBox(
height: controller.dragOffset.value < 0
? 0
: controller.dragOffset.value > 210.w
? 210.w
: controller.dragOffset.value,
child: Stack(
children: [
Positioned(
bottom: 0,
child: Container(
width: 1.sw,
height: 200.w,
margin: EdgeInsets.only(bottom: 10.w),
decoration: BoxDecoration(
color: Colors.grey[200]!,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(20.r),
),
),
child: Column(
children: [
Container(
margin: EdgeInsets.only(bottom: 5.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
),
child: AudioFileWaveforms(
size: Size(350.w, 120.w),
playerController: controller.playerController,
playerWaveStyle: PlayerWaveStyle(
fixedWaveColor: Colors.grey[300]!,
liveWaveColor: Colors.red,
seekLineColor: Colors.red,
scaleFactor: 600,
),
),
),
SizedBox(
width: 350.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Obx(
() => Text(
DateUtil.formatDateMs(
controller.currentDuration.value,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
),
),
),
Obx(
() => Text(
DateUtil.formatDateMs(
controller.durationMax.value,
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: isDarkMode
? Colors.grey[400]
: Colors.grey[500],
fontSize: 12.sp,
),
),
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: () {
int progress =
controller.currentDuration.value - 10000;
if (progress < 0) {
progress = 0;
}
controller.playerController.seekTo(progress);
},
child: Icon(
Icons.replay_10,
color: Colors.black,
size: 30.w,
),
),
StreamBuilder<PlayerState>(
stream:
controller.playerController.onPlayerStateChanged,
builder: (context, snapshot) {
bool isPlay = snapshot.data?.isPlaying ?? false;
return GestureDetector(
onTap: () {
if (isPlay) {
controller.playerController.pausePlayer();
} else {
controller.playerController.startPlayer();
}
},
child: Icon(
isPlay ? Icons.pause_circle : Icons.play_circle,
color: Colors.black,
size: 48.w,
),
);
},
),
GestureDetector(
onTap: () {
int progress =
controller.currentDuration.value + 10000;
controller.playerController.seekTo(progress);
},
child: Icon(
Icons.forward_10,
color: Colors.black,
size: 30.w,
),
),
],
),
],
),
),
),
],
),
),
);
}
Widget _iconItem(IconData icon, String title, int index) {
bool isActive = controller.currentIndex.value == index;
@ -337,9 +406,6 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
onTap: () {
if (isActive) return;
controller.tabController.index = index;
controller.isSpeakerText.value = false;
controller.editSpeakerTextId.value = 0;
controller.editSpeakerTextList = [];
},
child: Container(
height: 45.w,

101
lib/modules/meeting/views/mind_map_tab.dart

@ -1,12 +1,107 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MindMapTab extends StatelessWidget {
import '../controllers/meeting_details_controller.dart';
class MindMapTab extends StatefulWidget {
const MindMapTab({super.key});
@override
State<MindMapTab> createState() => _MindMapTabState();
}
class _MindMapTabState extends State<MindMapTab>
with AutomaticKeepAliveClientMixin {
late final WebViewController _controller;
final _meetingDetailsController = Get.find<MeetingDetailsController>();
@override
void initState() {
super.initState();
String html = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MindMap</title>
<script>
window.markmap = {
autoLoader: {
toolbar: true,
},
};
</script>
<script src="https://cdn.jsdelivr.net/npm/markmap-autoloader@latest"></script>
<style>
html,
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
.markmap {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.markmap>svg {
width: 100%;
height: 100%;
}
.markmap>.mm-toolbar {
top: 20px !important;
bottom: initial !important;
}
.markmap>.mm-toolbar>.mm-toolbar-brand {
display: none;
}
.markmap>.mm-toolbar>.mm-toolbar-item:nth-child(2) {
margin-left: 0;
}
</style>
</head>
<body>
<div class="markmap">
<script type="text/template">
---
markmap:
maxWidth: 300
colorFreezeLevel: 2
initialExpandLevel: 3
---
${_meetingDetailsController.meetingDetails.value.summary}
</script>
</div>
</body>
</html>
''';
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.transparent)
..loadHtmlString(html);
}
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Text(''),
super.build(context);
return WebViewWidget(
controller: _controller,
);
}
}

573
lib/modules/meeting/views/speaker_all_bottom_sheet.dart

@ -0,0 +1,573 @@
import 'package:common_utils/common_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../controllers/meeting_details_controller.dart';
class SpeakerAllBottomSheet extends StatefulWidget {
const SpeakerAllBottomSheet({super.key});
@override
State<SpeakerAllBottomSheet> createState() => _SpeakerAllBottomSheetState();
}
class _SpeakerAllBottomSheetState extends State<SpeakerAllBottomSheet> {
final _controller = Get.find<MeetingDetailsController>();
final GetStorage _storage = GetStorage();
List _dataList = [];
List _historySpeakerList = [];
List _speakerList = [];
@override
void initState() {
super.initState();
_controller.playerController.pausePlayer();
_controller.speakerAllStarttime.value = -1;
_controller.speakerAllEndtime.value = -1;
_getSpeakerList();
}
@override
void dispose() {
_controller.playerController.pausePlayer();
_controller.speakerAllStarttime.value = -1;
_controller.speakerAllEndtime.value = -1;
super.dispose();
}
void _getSpeakerList() {
_historySpeakerList = _storage.read("speaker_list") ?? [];
_speakerList = List.from(_historySpeakerList);
List textList = _controller.textList;
List dataList = [];
for (var item in textList) {
String speaker = item['speaker'];
Map? foundElement = dataList.firstWhere(
(element) => element['speaker'] == speaker,
orElse: () => null,
);
if (foundElement != null) {
int time = item['endtime'] - item['starttime'];
foundElement['totaltime'] = foundElement['totaltime'] + time;
if (!foundElement['isOmit']) {
if (foundElement['list'].length < 2) {
foundElement['list'].add(Map.from(item));
} else {
foundElement['isOmit'] = true;
}
}
} else {
dataList.add({
'speaker': speaker,
'speakerText': TextEditingController(),
'isShow': false,
'totaltime': item['endtime'] - item['starttime'],
'isOmit': false,
'list': [Map.from(item)],
});
}
if (speaker.isNotEmpty &&
!RegExp(r'^Speaker \d+$').hasMatch(speaker) &&
!_speakerList.contains(speaker)) {
_speakerList.add(speaker);
}
}
setState(() {
_dataList = dataList;
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
left: 12.w,
right: 12.w,
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'命名发言者',
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
fontWeight: FontWeight.bold,
),
),
GestureDetector(
onTap: () => Get.back(),
child: Container(
width: 26.w,
height: 26.w,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26.r),
),
child: Icon(
Icons.close,
size: 18.w,
color: Colors.grey[400],
),
),
)
],
),
),
Divider(
height: 1,
color: Colors.grey[300],
),
Container(
constraints: BoxConstraints(
maxHeight:
1.sh - 240.w - MediaQuery.of(context).viewInsets.bottom,
),
child: ListView.builder(
itemCount: _dataList.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
Map item = _dataList[index];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
margin: EdgeInsets.only(top: 15.w),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: item['isShow'] && _speakerList.isNotEmpty
? BorderRadius.vertical(
top: Radius.circular(12.r),
)
: BorderRadius.circular(12.r),
),
child: Focus(
onFocusChange: (bool hasFocus) {
setState(() {
item['isShow'] = hasFocus;
});
},
child: TextField(
controller: item['speakerText'],
style: TextStyle(
fontSize: 16.sp,
color: Colors.black87,
letterSpacing: 0,
height: 1.4,
),
decoration: InputDecoration(
hintText: item['speaker'],
hintStyle: TextStyle(
fontSize: 16.sp,
color: Colors.grey[400],
letterSpacing: 0,
height: 1.4,
),
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 12.w,
),
),
),
),
),
if (item['isShow'] && _speakerList.isNotEmpty)
Container(
padding: EdgeInsets.only(left: 10.w),
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
bottom: Radius.circular(12.r),
),
border: Border(
top: BorderSide(
width: 1,
color: Colors.grey[300]!,
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 10.w, bottom: 5.w),
child: Text(
'最近使用过的名称',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey[600]!,
),
),
),
Container(
constraints: BoxConstraints(
maxHeight: 150.h,
),
child: ListView.builder(
itemCount: _speakerList.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int idx) {
return GestureDetector(
onTap: () {
item['speakerText'].text =
_speakerList[idx];
FocusScope.of(context).unfocus();
},
behavior: HitTestBehavior.translucent,
child: SizedBox(
height: 42.w,
child: Align(
alignment: Alignment.centerLeft,
child: Text(
_speakerList[idx],
style: TextStyle(
fontSize: 14.sp,
color: Colors.black87,
),
),
),
),
);
},
),
),
],
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 8.w,
),
child: Row(
children: [
Text(
'总计',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey,
),
),
5.horizontalSpace,
Text(
'${item['totaltime'] / 1000}s',
style: TextStyle(
fontSize: 12.sp,
color: Colors.black87,
),
),
],
),
),
...List.generate(
item['list'].length,
(int idx) {
Map data = item['list'][idx];
return Obx(() {
bool isActive = false;
if (_controller.speakerAllStarttime.value ==
data['starttime'] &&
_controller.speakerAllEndtime.value ==
data['endtime']) {
isActive = true;
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (isActive) {
_controller.playerController.pausePlayer();
_controller.speakerAllStarttime.value = -1;
_controller.speakerAllEndtime.value = -1;
} else {
_controller.speakerAllStarttime.value =
data['starttime'];
_controller.speakerAllEndtime.value =
data['endtime'];
_controller.playerController
.seekTo(data['starttime']);
_controller.playerController.startPlayer();
}
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
isActive
? Icons.pause_circle
: Icons.play_circle,
color: Colors.red[400],
size: 15.w,
),
2.horizontalSpace,
Text(
DateUtil.formatDateMs(
data['starttime'],
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: Colors.grey,
fontSize: 12.sp,
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 2.w),
child: Text(
'-',
style: TextStyle(
color: Colors.grey,
fontSize: 12.sp,
),
),
),
Text(
DateUtil.formatDateMs(
data['endtime'],
format: 'HH:mm:ss',
isUtc: true,
),
style: TextStyle(
color: Colors.grey,
fontSize: 12.sp,
),
),
5.horizontalSpace,
Text(
'${(data['endtime'] - data['starttime']) / 1000}s',
style: TextStyle(
color: Colors.black54,
fontSize: 12.sp,
),
),
],
),
Padding(
padding:
EdgeInsets.symmetric(vertical: 8.w),
child: Text(
data['content'],
overflow: TextOverflow.ellipsis,
maxLines: 2,
style: TextStyle(
color: isActive
? Colors.red
: Colors.grey[700],
fontSize: 12.sp,
),
),
),
],
),
),
);
});
},
),
if (item['isOmit'])
Padding(
padding:
EdgeInsets.only(left: 10.w, top: 5.w, bottom: 8.w),
child: Row(
children: List.generate(
6,
(int idx) => Container(
width: 3.w,
height: 3.w,
margin: EdgeInsets.only(right: 3.w),
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.circular(3.r),
),
),
),
),
),
Padding(
padding: EdgeInsets.only(top: 7.w),
child: _separator(),
),
],
);
},
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.w),
child: Text(
'总结的发言者将自动更新',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey[600],
),
),
),
Padding(
padding: EdgeInsets.only(bottom: 20.w),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'取消',
style: TextStyle(
fontSize: 16.sp,
color: Colors.black,
),
),
),
),
),
10.horizontalSpace,
Expanded(
child: GestureDetector(
onTap: _save,
child: Container(
height: 48.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12.r),
),
child: Text(
'确认',
style: TextStyle(
fontSize: 16.sp,
color: Colors.white,
),
),
),
),
),
],
),
),
],
),
);
}
Widget _separator() {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final boxWidth = constraints.constrainWidth();
const dashWidth = 2.0;
const dashHeight = 1.0;
final dashCount = (boxWidth / (2 * dashWidth)).floor();
return Flex(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
direction: Axis.horizontal,
children: List.generate(dashCount, (_) {
return SizedBox(
width: dashWidth,
height: dashHeight,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.grey[300]),
),
);
}),
);
},
);
}
void _save() async {
EasyLoading.show();
List textList = _controller.textList;
List newTextList = [];
List editSpeakerList = [];
List personnelList = [];
String personnel = '';
bool isLoopEnd = false;
for (var element in textList) {
final dataMap = Map<String, dynamic>.from(element);
for (var data in _dataList) {
String speaker = data['speakerText'].text;
if (speaker.isNotEmpty && speaker != data['speaker']) {
if (!isLoopEnd) {
personnelList.add({
'original': data['speaker'],
'speaker': speaker,
});
}
if (dataMap['speaker'] == data['speaker']) {
dataMap['speaker'] = speaker;
editSpeakerList.add({
'id': dataMap['id'],
'speaker': dataMap['speaker'],
});
}
if (!RegExp(r'^Speaker \d+$').hasMatch(speaker) &&
!_historySpeakerList.contains(speaker)) {
_historySpeakerList.insert(0, speaker);
}
}
}
if (!personnel.contains(dataMap['speaker'])) {
personnel += '[${dataMap['speaker']}] ';
}
newTextList.add(dataMap);
isLoopEnd = true;
}
if (editSpeakerList.isNotEmpty) {
var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList);
if (result != null) {
bool isEditSummary = false;
String summaryText = _controller.meetingDetails.value.summary;
_controller.textList.value = newTextList;
for (var element in personnelList) {
if (summaryText.contains('[${element['original']}]')) {
summaryText = summaryText.replaceAll(
'[${element['original']}]', '[${element['speaker']}]');
isEditSummary = true;
}
}
if (isEditSummary) {
SqfliteApi.editMeeting(_controller.id, {
'summary': summaryText,
'personnel': personnel,
});
_controller.meetingDetails.value.summary = summaryText;
} else {
SqfliteApi.editMeeting(_controller.id, {'personnel': personnel});
}
_controller.meetingDetails.value.personnel = personnel;
_controller.meetingDetails.refresh();
_storage.write("speaker_list", _historySpeakerList);
Get.back();
}
} else {
Get.back();
}
EasyLoading.dismiss();
}
}

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

@ -171,7 +171,6 @@ class _SpeakerBottomSheetState extends State<SpeakerBottomSheet> {
},
behavior: HitTestBehavior.translucent,
child: SizedBox(
width: 200.w,
height: 42.w,
child: Align(
alignment: Alignment.centerLeft,
@ -366,7 +365,19 @@ class _SpeakerBottomSheetState extends State<SpeakerBottomSheet> {
var result = await SqfliteApi.editMeetingSpeaker(editSpeakerList);
if (result != null) {
_controller.textList.value = newTextList;
SqfliteApi.editMeeting(_controller.id, {'personnel': personnel});
if (_controller.meetingDetails.value.summary
.contains('[${widget.speakerName}]')) {
String summaryText = _controller.meetingDetails.value.summary
.replaceAll(
'[${widget.speakerName}]', '[${_speakerController.text}]');
SqfliteApi.editMeeting(_controller.id, {
'summary': summaryText,
'personnel': personnel,
});
_controller.meetingDetails.value.summary = summaryText;
} else {
SqfliteApi.editMeeting(_controller.id, {'personnel': personnel});
}
_controller.meetingDetails.value.personnel = personnel;
_addHistorySpeaker();
Get.back();

69
lib/modules/meeting/views/speech_tab.dart

@ -4,6 +4,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../controllers/meeting_details_controller.dart';
import 'speaker_all_bottom_sheet.dart';
import 'speaker_bottom_sheet.dart';
class SpeechTab extends GetView<MeetingDetailsController> {
@ -20,7 +21,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
itemBuilder: (BuildContext context, int index) {
Map item = controller.textList[index];
TextStyle textStyle = TextStyle(
fontSize: 16.sp,
fontSize: 15.sp,
color: Colors.black87,
letterSpacing: 0,
height: 1.4,
@ -34,11 +35,11 @@ class SpeechTab extends GetView<MeetingDetailsController> {
children: [
if (item['speaker'].isNotEmpty)
Container(
width: 12.w,
height: 12.w,
margin: EdgeInsets.only(right: 2.w),
width: 11.w,
height: 11.w,
margin: EdgeInsets.only(right: 3.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28.r),
borderRadius: BorderRadius.circular(11.r),
border: Border.all(
color: const Color(0xFFFFC107),
width: 3.w,
@ -70,7 +71,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
item['speaker'],
style: TextStyle(
color: const Color(0xFFFFC107),
fontSize: 14.sp,
fontSize: 13.sp,
),
),
),
@ -83,7 +84,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
),
style: TextStyle(
color: Colors.grey,
fontSize: 14.sp,
fontSize: 13.sp,
),
),
],
@ -150,16 +151,46 @@ class SpeechTab extends GetView<MeetingDetailsController> {
),
);
} else {
return Container(
padding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 5.w,
),
child: Text(
item['content'],
style: textStyle,
),
);
return Obx(() {
bool isActive = false;
int starttime = item['starttime'];
int endtime = 0;
if (index == controller.textList.length - 1) {
endtime = controller.durationMax.value;
} else {
endtime = controller.textList[index + 1]['starttime'];
}
if (controller.currentDuration.value != 0) {
if (controller.currentDuration.value >= starttime &&
controller.currentDuration.value < endtime) {
isActive = true;
}
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
controller.playerController
.seekTo(item['starttime']);
controller.playerController.startPlayer();
},
child: Container(
width: double.infinity,
padding: EdgeInsets.symmetric(
horizontal: 14.w,
vertical: 5.w,
),
child: Text(
item['content'],
style: TextStyle(
fontSize: 15.sp,
color: isActive ? Colors.red : Colors.black87,
letterSpacing: 0,
height: 1.4,
),
),
),
);
});
}
}),
],
@ -182,6 +213,8 @@ class SpeechTab extends GetView<MeetingDetailsController> {
'编辑',
() {
controller.isSpeakerText.value = true;
controller.dragOffset.value = 0;
controller.isTop.value = true;
},
),
if (controller.meetingDetails.value.tasktype == 1 &&
@ -200,7 +233,7 @@ class SpeechTab extends GetView<MeetingDetailsController> {
),
),
builder: (BuildContext context) {
return Container();
return const SpeakerAllBottomSheet();
},
);
},

215
lib/modules/meeting/views/summary_tab.dart

@ -4,14 +4,117 @@ import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../data/services/db/sqflite_api.dart';
import '../controllers/meeting_details_controller.dart';
import 'speaker_all_bottom_sheet.dart';
class SummaryTab extends GetView<MeetingDetailsController> {
class SummaryTab extends StatefulWidget {
const SummaryTab({super.key});
@override
State<SummaryTab> createState() => _SummaryTabState();
}
class _SummaryTabState extends State<SummaryTab> {
final _controller = Get.find<MeetingDetailsController>();
final String _addressPrefix = '地点:';
String _addressText = '';
final String _personnelPrefix = '与会人员:';
String _personnelText = '';
final TextEditingController _addressController = TextEditingController();
final TextEditingController _personnelController = TextEditingController();
@override
void initState() {
super.initState();
_addressController.text = _addressPrefix +
(_controller.meetingDetails.value.address.isNotEmpty
? _controller.meetingDetails.value.address
: '[输入地点]');
_personnelController.text = _personnelPrefix +
(_controller.meetingDetails.value.personnel.isNotEmpty
? _controller.meetingDetails.value.personnel
: '[输入与会人员]');
_addressController.addListener(_handleAddressChanged);
_personnelController.addListener(_handlePersonnelChanged);
}
void _handleAddressChanged() {
String currentText = _addressController.text;
TextSelection currentSelection = _addressController.selection;
if (!currentText.startsWith(_addressPrefix)) {
_addressController.value = TextEditingValue(
text: _addressPrefix + _addressText,
selection: TextSelection.collapsed(
offset: _addressPrefix.length + _addressText.length,
),
);
return;
} else {
_addressText = currentText.substring(_addressPrefix.length);
}
if (currentSelection.start < _addressPrefix.length ||
currentSelection.end < _addressPrefix.length) {
int newStart = currentSelection.start < _addressPrefix.length
? _addressPrefix.length
: currentSelection.start;
int newEnd = currentSelection.end < _addressPrefix.length
? _addressPrefix.length
: currentSelection.end;
_addressController.selection = currentSelection.copyWith(
baseOffset: newStart,
extentOffset: newEnd,
);
}
}
void _handlePersonnelChanged() {
String currentText = _personnelController.text;
TextSelection currentSelection = _personnelController.selection;
if (!currentText.startsWith(_personnelPrefix)) {
_personnelController.value = TextEditingValue(
text: _personnelPrefix + _personnelText,
selection: TextSelection.collapsed(
offset: _personnelPrefix.length + _personnelText.length,
),
);
return;
} else {
_personnelText = currentText.substring(_personnelPrefix.length);
}
if (currentSelection.start < _personnelPrefix.length ||
currentSelection.end < _personnelPrefix.length) {
int newStart = currentSelection.start < _personnelPrefix.length
? _personnelPrefix.length
: currentSelection.start;
int newEnd = currentSelection.end < _personnelPrefix.length
? _personnelPrefix.length
: currentSelection.end;
_personnelController.selection = currentSelection.copyWith(
baseOffset: newStart,
extentOffset: newEnd,
);
}
}
@override
void dispose() {
_addressController.dispose();
_personnelController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final TextStyle textStyle = TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontSize: 16.sp,
letterSpacing: 0,
height: 1.5,
);
return Stack(
children: [
@ -24,7 +127,7 @@ class SummaryTab extends GetView<MeetingDetailsController> {
Padding(
padding: EdgeInsets.symmetric(vertical: 10.w),
child: Text(
controller.meetingData.value.title,
_controller.meetingData.value.title,
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
@ -42,7 +145,7 @@ class SummaryTab extends GetView<MeetingDetailsController> {
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
controller.meetingData.value.type,
_controller.meetingData.value.type,
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[500],
@ -58,7 +161,7 @@ class SummaryTab extends GetView<MeetingDetailsController> {
2.horizontalSpace,
Text(
DateUtil.formatDateMs(
controller.meetingData.value.creationtime,
_controller.meetingData.value.creationtime,
format: DateFormats.y_mo_d_h_m,
),
style: TextStyle(
@ -74,7 +177,7 @@ class SummaryTab extends GetView<MeetingDetailsController> {
),
2.horizontalSpace,
Text(
'${controller.meetingData.value.seconds}s',
'${_controller.meetingData.value.seconds}s',
style: TextStyle(
color: isDarkMode ? Colors.grey[400] : Colors.grey[500],
fontSize: 12.sp,
@ -102,43 +205,79 @@ class SummaryTab extends GetView<MeetingDetailsController> {
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'日期和时间:${DateUtil.formatDateMs(
controller.meetingData.value.creationtime,
format: DateFormats.y_mo_d_h_m,
)}',
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontSize: 16.sp,
child: Focus(
onFocusChange: (bool hasFocus) {
if (!hasFocus) {
bool isEdit = false;
if (_addressText.isNotEmpty &&
_addressText !=
_controller.meetingDetails.value.address) {
_controller.meetingDetails.value.address =
_addressText;
isEdit = true;
}
if (_personnelText.isNotEmpty &&
_personnelText !=
_controller.meetingDetails.value.personnel) {
_controller.meetingDetails.value.personnel =
_personnelText;
isEdit = true;
}
if (isEdit) {
SqfliteApi.editMeeting(
_controller.id,
{
'address':
_controller.meetingDetails.value.address,
'personnel':
_controller.meetingDetails.value.personnel,
},
);
}
}
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'日期和时间:${DateUtil.formatDateMs(
_controller.meetingData.value.creationtime,
format: DateFormats.y_mo_d_h_m,
)}',
style: textStyle,
),
),
Text(
'地点:${controller.meetingDetails.value.address.isNotEmpty ? controller.meetingDetails.value.address : '[输入地点]'}',
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontSize: 16.sp,
TextField(
controller: _addressController,
style: textStyle,
minLines: 1,
maxLines: 100,
decoration: const InputDecoration(
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
Text(
'与会人员:${controller.meetingDetails.value.personnel.isNotEmpty ? controller.meetingDetails.value.personnel : '[输入与会人员]'}',
style: TextStyle(
color:
isDarkMode ? Colors.grey[400] : Colors.grey[700],
fontSize: 16.sp,
TextField(
controller: _personnelController,
style: textStyle,
minLines: 1,
maxLines: 100,
decoration: const InputDecoration(
isDense: true,
fillColor: Colors.transparent,
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
],
],
),
),
),
Padding(
padding: EdgeInsets.only(top: 10.w),
child: MarkdownBody(
data: controller.meetingDetails.value.summary,
data: _controller.meetingDetails.value.summary,
),
),
],
@ -153,7 +292,7 @@ class SummaryTab extends GetView<MeetingDetailsController> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (controller.meetingDetails.value.summary.isNotEmpty)
if (_controller.meetingDetails.value.summary.isNotEmpty)
_button(
Icons.supervisor_account,
'编辑发言者',
@ -168,18 +307,18 @@ class SummaryTab extends GetView<MeetingDetailsController> {
),
),
builder: (BuildContext context) {
return Container();
return const SpeakerAllBottomSheet();
},
);
},
),
if (controller.meetingDetails.value.summary.isEmpty)
if (_controller.meetingDetails.value.summary.isEmpty)
SizedBox(
width: 150.w,
child: _button(
Icons.auto_awesome,
'生成',
controller.summaryCreate,
_controller.summaryCreate,
),
),
],

9
lib/modules/navigation/bindings/navigation_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/navigation_controller.dart';
class NavigationBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<NavigationController>(() => NavigationController());
}
}

5
lib/modules/navigation/controllers/navigation_controller.dart

@ -0,0 +1,5 @@
import 'package:get/get.dart';
class NavigationController extends GetxController {
static NavigationController get to => Get.find(); //单例访问模式
}

17
lib/modules/navigation/views/navigation_view.dart

@ -0,0 +1,17 @@
import 'package:deep_voice/modules/navigation/controllers/navigation_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class NavigationView extends GetView<NavigationController> {
const NavigationView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('地图展示')),
body: Stack(
children: [],
),
);
}
}

124
lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart

@ -22,7 +22,7 @@ class OtaUpgradeController extends GetxController {
final isUpdateAvailable = true.obs;
final RxDouble progress = 0.0.obs;
final JlOta jla = JlOta();
// 蓝牙服务
final _bleManager = Get.find<BleManager>();
@override
@ -53,68 +53,65 @@ class OtaUpgradeController extends GetxController {
}
Future<void> _getCurrentVersion() async {
JlOta().getPlatformVersion();
//try {
// // 添加获取设备信息的重试机制
// DeviceInfo? deviceInfo = _bleManager.deviceInfo.value;
// // 第一次检查设备信息
// if (deviceInfo == null) {
// // 显示加载提示
// isFetchingDeviceInfo.value = true;
// // 尝试重新获取设备信息
// await _bleManager.fetchDeviceInfo();
// // 等待后再次获取设备信息
// deviceInfo = _bleManager.deviceInfo.value;
// isFetchingDeviceInfo.value = false;
// // 仍然为空则抛出异常
// if (deviceInfo == null) {
// throw const FormatException('无法获取设备信息,请检查设备连接');
// }
// }
// // 检查固件信息有效性
// if (deviceInfo.firmwareVersion.isEmpty
// // ||
// // deviceInfo.firmwareProductId.isEmpty
// ) {
// throw const FormatException('设备固件信息不完整');
// }
// // 正常流程
// currentVersion.value = "0"; //deviceInfo.firmwareVersion;
// // API请求
// final response = await Api.getequipmentinfo({
// "id": "s3",
// });
// // 处理API响应
// if (response['info'] == null) {
// throw const FormatException('API 响应格式错误');
// }
// final resp = OtaConfigResp.fromJson(response);
// latestVersion.value = resp.systemversion!;
// updatepackageaddress = resp.updatepackageaddress!;
// // 版本检查
// isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!);
// } catch (e) {
// // 统一错误处理
// errorMessage.value = e.toString();
// Get.snackbar('错误', e.toString(),
// snackPosition: SnackPosition.BOTTOM,
// backgroundColor: Colors.red,
// colorText: Colors.white);
// // 重要:重新抛出异常以便上层处理(可选)
// } finally {
// isFetchingDeviceInfo.value = false;
// }
try {
// 添加获取设备信息的重试机制
DeviceInfo? deviceInfo = _bleManager.deviceInfo.value;
// 第一次检查设备信息
if (deviceInfo == null) {
// 显示加载提示
isFetchingDeviceInfo.value = true;
// 尝试重新获取设备信息
await _bleManager.fetchDeviceInfo();
// 等待后再次获取设备信息
deviceInfo = _bleManager.deviceInfo.value;
isFetchingDeviceInfo.value = false;
// 仍然为空则抛出异常
if (deviceInfo == null) {
throw const FormatException('无法获取设备信息,请检查设备连接');
}
}
// 检查固件信息有效性
if (deviceInfo.firmwareVersion.isEmpty ||
deviceInfo.firmwareProductId.isEmpty) {
throw const FormatException('设备固件信息不完整');
}
// 正常流程
currentVersion.value = deviceInfo.firmwareVersion;
print('getequipmentinfo,id: ${deviceInfo.firmwareProductId}');
// API请求
final response = await Api.getequipmentinfo({
"id": deviceInfo.firmwareProductId,
});
// 处理API响应
if (response['info'] == null) {
throw const FormatException('API 响应格式错误');
}
final resp = OtaConfigResp.fromJson(response);
print('getequipmentinfo,resp.systemversion: ${resp.systemversion}');
latestVersion.value = "1";
updatepackageaddress = resp.updatepackageaddress!;
// 版本检查
isUpdateAvailable.value = _isNewVersionAvailable(resp.systemversion!);
} catch (e) {
// 统一错误处理
errorMessage.value = e.toString();
Get.snackbar('错误', e.toString(),
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white);
// 重要:重新抛出异常以便上层处理(可选)
} finally {
isFetchingDeviceInfo.value = false;
}
}
// 版本比较方法优化
@ -295,6 +292,7 @@ class OtaUpgradeController extends GetxController {
// 开始升级
void _installApp(String path) async {
jla.getPlatformVersion();
// JlOtaPlugin()
// final File file = File(path);
// if (await file.exists()) {

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

@ -9,13 +9,12 @@ import '../../../data/services/bluetooth_manager.dart';
import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice;
import '../../../data/services/ble_manager.dart';
import 'package:ble_service/ble_service.dart';
import 'package:flutter/material.dart';
/// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程
class PairingController extends GetxController {
static PairingController get to => Get.find();
// 需要设备
final bool needdeivice = false;
// 状态标志
final isLoading = false.obs;
@ -64,6 +63,9 @@ class PairingController extends GetxController {
// BLE服务
final _bleService = BleService();
// 日志tag
static const _tag = 'PairingController';
@override
void onInit() {
super.onInit();
@ -83,6 +85,11 @@ class PairingController extends GetxController {
isLoading.value = true;
statusDescription.value = 'checkingPermissions'.tr;
// 在iOS上,延迟一下再请求权限,避免多权限冲突
if (Platform.isIOS) {
await Future.delayed(Duration(milliseconds: 800));
}
// 检查并请求权限
if (!await _checkAndRequestPermissions()) {
statusDescription.value = 'permissionDenied'.tr;
@ -96,7 +103,7 @@ class PairingController extends GetxController {
// 检查蓝牙状态并刷新设备
await _checkBluetoothAndRefresh();
} catch (e) {
Logger.error('初始化配对控制器失败: $e');
Logger.e(_tag, '初始化配对控制器失败: ${e.toString()}');
statusDescription.value = 'initializationFailure'.tr;
} finally {
isLoading.value = false;
@ -128,13 +135,12 @@ class PairingController extends GetxController {
/// 检查设备配对状态
Future<void> _checkDevicePairingStatus() async {
// 只在Android平台且设备已连接时执行
if (!Platform.isAndroid || !isDeviceConnected) return;
if (!isDeviceConnected) return;
try {
final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) {
Logger.info('设备不支持Companion功能');
Logger.i(_tag, '设备不支持Companion功能');
_companionAssociated.value = true; // 不支持则视为已配对
_userRejectedAssociation.value = false; // 重置拒绝状态
return;
@ -143,16 +149,16 @@ class PairingController extends GetxController {
final pairedMac = await _bleService.getPairedMacAddress();
if (pairedMac == null) {
// 没有配对记录,需要开始配对
Logger.info('无配对记录,需要开始设备配对');
Logger.i(_tag, '无配对记录,需要开始设备配对');
return;
}
Logger.info('发现已关联的Companion设备: $pairedMac');
Logger.i(_tag, '发现已关联的Companion设备: $pairedMac');
// 检查当前连接设备与已保存的设备是否一致
if (currentDevice!.address != pairedMac) {
// MAC地址不匹配
Logger.warning('当前连接的设备与已配对的DeepSound设备不符');
Logger.w(_tag, '当前连接的设备与已配对的DeepSound设备不符');
statusDescription.value = 'pleaseConnectPairedDevice'.tr;
return;
}
@ -164,13 +170,13 @@ class PairingController extends GetxController {
}
// 设备匹配且合规,标记为已配对
Logger.info('当前设备与已配对设备匹配');
Logger.i(_tag, '当前设备与已配对设备匹配');
_companionAssociated.value = true;
_userRejectedAssociation.value = false; // 重置拒绝状态
await _bleService.registerBackgroundScan();
_autoNavigateToHome();
} catch (e) {
Logger.error('检查设备配对状态失败: $e');
Logger.e(_tag, '检查设备配对状态失败: ${e.toString()}');
}
}
@ -186,27 +192,121 @@ class PairingController extends GetxController {
if (Platform.isAndroid) {
permissionsToRequest.add(Permission.locationWhenInUse);
permissionsToRequest.add(Permission.microphone);
// permissionsToRequest.add(Permission.microphone);
if (await _getAndroidSdkVersion() >= 31) {
permissionsToRequest.add(Permission.bluetoothScan);
permissionsToRequest.add(Permission.bluetoothConnect);
}
} else if (Platform.isIOS) {
// iOS需要请求的权限
permissionsToRequest.add(Permission.bluetooth);
permissionsToRequest.add(Permission.location);
// 如果需要麦克风权限(例如语音功能)
// permissionsToRequest.add(Permission.microphone);
try {
// 先尝试请求权限
if (permissionsToRequest.isNotEmpty) {
statuses = await permissionsToRequest.request();
bool allGranted = true;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
Logger.w(_tag, 'iOS权限 ${permission.toString()} 未被授予: $status');
// 特殊处理蓝牙权限
if (permission == Permission.bluetooth) {
Logger.w(_tag, 'iOS蓝牙权限未授予,状态: $status');
}
}
});
// 如果有权限未授予,但不是永久拒绝,继续尝试
if (!allGranted) {
bool hasPermanentlyDenied = statuses.values.any((status) => status.isPermanentlyDenied);
if (hasPermanentlyDenied) {
// 有权限被永久拒绝,需要引导用户去设置
_showPermissionSettingsDialog('部分权限被永久拒绝,请在设置中启用');
return false;
}
}
}
// 直接尝试使用蓝牙功能,这会触发iOS原生权限请求
bool isEnabled = await _bluetoothService.isBluetoothEnabled();
Logger.i(_tag, 'iOS蓝牙状态检查: ${isEnabled ? "已开启" : "未开启"}');
// 检测AccessorySetupKit支持情况(iOS 18+特性)
bool companionSupported = await _bleService.isCompanionSupported();
Logger.i(_tag, 'iOS Companion支持检查: ${companionSupported ? "支持" : "不支持"}');
if (companionSupported) {
// 针对iOS 18+,尝试预热AccessorySetupKit框架
try {
// 此调用会触发AccessorySetupKit相关权限请求
final pairedMac = await _bleService.getPairedMacAddress();
Logger.i(_tag, '获取已配对设备MAC: ${pairedMac ?? "无"}');
} catch (e) {
Logger.w(_tag, 'iOS 18+ AccessorySetupKit初始化失败: ${e.toString()}');
}
}
// 在iOS上视为权限已授予
_hasPermissions.value = true;
return true;
} catch (e) {
Logger.e(_tag, "iOS蓝牙初始化失败: ${e.toString()}");
// 显示自定义对话框引导用户
_showPermissionSettingsDialog('需要开启蓝牙权限才能配对设备');
return false;
}
}
statuses = await permissionsToRequest.request();
bool allGranted = true;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
Logger.warning('权限 ${permission.toString()} 未被授予: $status');
// 如果还有其他权限需要请求
if (permissionsToRequest.isNotEmpty) {
statuses = await permissionsToRequest.request();
bool allGranted = true;
statuses.forEach((permission, status) {
if (!status.isGranted) {
allGranted = false;
Logger.w(_tag, '权限 ${permission.toString()} 未被授予: $status');
}
});
if (!allGranted) {
return false;
}
});
}
_hasPermissions.value = allGranted;
return allGranted;
_hasPermissions.value = true;
return true;
}
/// 显示权限设置对话框
void _showPermissionSettingsDialog(String message) {
Get.dialog(
AlertDialog(
title: Text('需要权限'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('稍后'),
),
TextButton(
onPressed: () {
Get.back();
openBluetoothSettings();
},
child: Text('去设置'),
),
],
),
barrierDismissible: false,
);
}
// 辅助方法获取Android SDK版本
@ -230,14 +330,11 @@ class PairingController extends GetxController {
_deviceWorker =
ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) {
if (device != null && !_isNavigatingToHome) {
Logger.info('配对页面发现设备已连接: ${device.name}');
Logger.i(_tag, '配对页面发现设备已连接: ${device.name}');
_handleDeviceConnected();
}
_updateStatusDescription();
});
if (needdeivice) {
_handleDeviceConnected();
}
}
/// 处理设备连接成功事件
@ -296,9 +393,8 @@ class PairingController extends GetxController {
/// 启动Companion设备关联流程
Future<void> _startCompanionAssociation() async {
// 如果不是Android平台,已跳转,已完成关联或正在进行关联,直接返回
if (!Platform.isAndroid ||
_isNavigatingToHome ||
// 如果已跳转,已完成关联或正在进行关联,直接返回
if (_isNavigatingToHome ||
_companionAssociated.value ||
_isAssociating ||
!isDeviceConnected) {
@ -316,7 +412,7 @@ class PairingController extends GetxController {
// 检查是否支持Companion功能
final isSupported = await _bleService.isCompanionSupported();
if (!isSupported) {
Logger.info('设备不支持Companion功能,跳过关联');
Logger.i(_tag, '设备不支持Companion功能,跳过关联');
_companionAssociated.value = true;
_autoNavigateToHome();
return;
@ -329,7 +425,7 @@ class PairingController extends GetxController {
return;
}
Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
Logger.i(_tag, '开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
var success = await _bleService.startDeviceAssociation(
deviceNamePattern: currentDevice!.name,
@ -338,7 +434,7 @@ class PairingController extends GetxController {
_isAssociating = false;
if (success) {
Logger.info('Companion设备关联成功');
Logger.i(_tag, 'Companion设备关联成功');
_companionAssociated.value = true;
_userRejectedAssociation.value = false;
@ -349,14 +445,14 @@ class PairingController extends GetxController {
// 关联成功后跳转
_autoNavigateToHome();
} else {
Logger.error('Companion设备关联失败或被用户拒绝');
Logger.e(_tag, 'Companion设备关联失败或被用户拒绝');
// 关联失败 - 用户拒绝关联
_userRejectedAssociation.value = true;
// 确保UI状态更新
_updateStatusDescription();
}
} catch (e) {
Logger.error('Companion设备关联过程发生错误: $e');
Logger.e(_tag, 'Companion设备关联过程发生错误: ${e.toString()}');
_isAssociating = false;
_userRejectedAssociation.value = true;
_updateStatusDescription();
@ -365,13 +461,13 @@ class PairingController extends GetxController {
/// 自动跳转到主页
Future<void> _autoNavigateToHome() async {
if (needdeivice && (_isNavigatingToHome || !isDeviceConnected)) return;
if (_isNavigatingToHome || !isDeviceConnected) return;
// 启动BLE后台服务
await _startAgentService();
_isNavigatingToHome = true;
Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
Logger.i(_tag, '已找到连接的设备 ${currentDevice!.name},自动跳转到主页');
Future.delayed(const Duration(milliseconds: 500), () {
Get.offAllNamed(Routes.home);
@ -381,12 +477,13 @@ class PairingController extends GetxController {
/// 启动BLE后台服务
Future<void> _startAgentService() async {
try {
if (needdeivice && !isDeviceConnected) return;
// 注释掉设备连接检查,确保无论是否连接设备都能启动服务
// if (!isDeviceConnected) return;
final result = await BleManager.to.startAgentService();
Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
Logger.i(_tag, result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
} catch (e) {
Logger.error('启动BLE后台服务发生错误: $e');
Logger.e(_tag, '启动BLE后台服务发生错误: ${e.toString()}');
}
}
@ -394,9 +491,9 @@ class PairingController extends GetxController {
Future<void> _stopAgentService() async {
try {
final result = await BleManager.to.stopAgentService();
Logger.info(result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
Logger.i(_tag, result ? 'BLE后台服务停止成功' : 'BLE后台服务停止失败');
} catch (e) {
Logger.error('停止BLE后台服务发生错误: $e');
Logger.e(_tag, '停止BLE后台服务发生错误: ${e.toString()}');
}
}
@ -419,18 +516,93 @@ class PairingController extends GetxController {
} else if (Platform.isAndroid && !isBluetoothOn) {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
} else if (Platform.isIOS) {
openAppSettings();
// iOS上优先尝试打开蓝牙设置
if (!isBluetoothOn) {
// iOS上无法直接打开蓝牙设置,只能打开应用设置
AppSettings.openAppSettings();
} else {
// 如果蓝牙开启但有其他权限问题,打开应用设置
openAppSettings();
}
} else {
AppSettings.openAppSettings(type: AppSettingsType.bluetooth);
}
} catch (e) {
Logger.error('打开设置失败: $e');
Logger.e(_tag, '打开设置失败: ${e.toString()}');
}
}
/// 重新初始化蓝牙
Future<void> reinitializeBluetooth() async {
try {
isLoading.value = true;
statusDescription.value = '正在重新初始化蓝牙...';
// iOS上尝试重置权限状态
if (Platform.isIOS) {
await Future.delayed(Duration(milliseconds: 500));
// 检查蓝牙权限
PermissionStatus bluetoothStatus = await Permission.bluetooth.status;
Logger.i(_tag, "iOS蓝牙权限当前状态: $bluetoothStatus");
if (bluetoothStatus.isPermanentlyDenied) {
Logger.i(_tag, "iOS蓝牙权限处于永久拒绝状态,提示用户手动开启");
_showPermissionSettingsDialog('蓝牙权限被拒绝,请在设置中手动开启');
return;
}
// 尝试重新请求权限
if (!bluetoothStatus.isGranted) {
bluetoothStatus = await Permission.bluetooth.request();
Logger.i(_tag, "iOS蓝牙权限请求结果: $bluetoothStatus");
}
// 尝试检查蓝牙状态,这会重新触发系统权限弹窗
try {
bool isEnabled = await _bluetoothService.isBluetoothEnabled();
Logger.i(_tag, "iOS蓝牙状态: ${isEnabled ? '开启' : '关闭'}");
} catch (e) {
Logger.e(_tag, "iOS蓝牙状态检查失败: ${e.toString()}");
}
// 检查Companion功能(iOS 18+)
try {
bool isSupported = await _bleService.isCompanionSupported();
if (isSupported) {
Logger.i(_tag, "iOS 18+ Companion功能可用,尝试初始化");
// 尝试预热AccessorySetupKit
await _bleService.getPairedMacAddress();
}
} catch (e) {
Logger.w(_tag, "iOS Companion功能检查失败: ${e.toString()}");
}
}
// 重新检查权限
if (await _checkAndRequestPermissions()) {
// 重新检查蓝牙状态
await _checkBluetoothAndRefresh();
}
} catch (e) {
Logger.e(_tag, '重新初始化蓝牙失败: ${e.toString()}');
} finally {
isLoading.value = false;
_updateStatusDescription();
}
}
/// 打开应用设置
Future<void> openAppSettings() async {
await AppSettings.openAppSettings();
try {
await AppSettings.openAppSettings();
// 返回后重新检查权限状态
Future.delayed(Duration(milliseconds: 500), () {
reinitializeBluetooth();
});
} catch (e) {
Logger.e(_tag, '打开应用设置失败: ${e.toString()}');
}
}
/// 重新开始关联
@ -442,9 +614,13 @@ class PairingController extends GetxController {
/// 跳过连接
void skipConnecting() async {
// 启动BLE后台服务
// 即使没有设备连接,也启动BLE后台服务
await _startAgentService();
_isNavigatingToHome = true;
// 记录跳过配对的日志
Logger.i(_tag, '用户跳过设备配对,直接跳转到主页');
Get.offAllNamed(Routes.home);
}
}
}

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

@ -1,3 +1,4 @@
import 'package:dio/dio.dart' as dio;
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:flutter/material.dart';
@ -14,6 +15,7 @@ import 'package:dio/dio.dart';
import '../../../routes/app_routes.dart';
import '../../../modules/login/controllers/googleLogin.dart';
import '../../../modules/login/controllers/facebookLogin.dart';
//import 'package:http_parser/http_parser.dart'; // 新增此导入
class ProfileController extends GetxController {
static ProfileController get to => Get.find();
@ -150,25 +152,29 @@ class ProfileController extends GetxController {
// 更新用户信息到后端
Future<void> _syncWithBackend() async {
try {
// 准备数据
Map<String, dynamic> data = {
"image": "", // 默认空字符串
"language": currentLanguage.value, //当前用户语言
"name": userInfo.value.name, //当前用户名
};
// 如果有头像,转换为base64
// 创建FormData对象用于上传
dio.FormData formData = dio.FormData();
// 添加基本用户信息
formData.fields.add(MapEntry('language', currentLanguage.value));
formData.fields.add(MapEntry('name', userInfo.value.name));
// 如果有头像,添加到FormData
if (localAvatar.value.isNotEmpty) {
//如果本地头像路径不为空,即代表有头像
try {
File imageFile = File(localAvatar.value);
if (await imageFile.exists()) {
List<int> imageBytes = await imageFile.readAsBytes();
String base64Image = base64Encode(imageBytes);
data["image"] = base64Image;
// // 验证base64数据 - 将base64转回图片并保存到本地
// await _verifyBase64Image(base64Image);
// 添加文件到FormData
formData.files.add(
MapEntry(
'file',
await dio.MultipartFile.fromFile(
imageFile.path,
filename: 'avatar.jpg',
),
),
);
Logger().d('已添加头像文件到请求');
} else {
Logger().e('头像文件不存在');
}
@ -177,9 +183,10 @@ class ProfileController extends GetxController {
}
}
Logger().d('准备更新用户信息: $data');
// 调用API
final response = await Api.updateUserSetting(data);
Logger().d('准备更新用户信息: ${formData.fields}');
// 调用API,使用FormData
final response = await Api.updateUserSetting(formData);
Logger().d('--更新用户信息响应: $response');
if (response != null) {
Logger().d('更新成功: $response');

78
lib/modules/settings/controllers/settings_controller.dart

@ -12,6 +12,9 @@ import '../../../data/models/user_Info.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:async';
import 'package:app_settings/app_settings.dart';
class SettingsController extends GetxController {
// 语言设置
@ -101,6 +104,81 @@ class SettingsController extends GetxController {
final appVersion = ''.obs;
final buildNumber = ''.obs;
// 权限相关
// 权限组,用于在UI中显示
final List<Map<String, dynamic>> permissionGroups = [
{
'name': '通讯权限',
'icon': Icons.phone_android,
'permissions': [
{'type': Permission.phone, 'name': '电话', 'description': '用于拨打电话功能'},
{'type': Permission.sms, 'name': '短信', 'description': '用于发送短信功能'},
{'type': Permission.contacts, 'name': '通讯录', 'description': '用于访问联系人'}
]
},
{
'name': '日程权限',
'icon': Icons.calendar_today,
'permissions': [
{'type': Permission.calendarWriteOnly, 'name': '日历写入', 'description': '用于创建日历事件和提醒'},
{'type': Permission.calendarFullAccess, 'name': '日历完全访问', 'description': '用于读取和修改日历事件'}
]
},
{
'name': '存储权限',
'icon': Icons.storage,
'permissions': [
{'type': Permission.storage, 'name': '存储', 'description': '用于保存录音和文件'},
{'type': Permission.photos, 'name': '照片', 'description': '用于选择和保存照片'}
]
},
{
'name': '位置权限',
'icon': Icons.location_on,
'permissions': [
{'type': Permission.location, 'name': '位置', 'description': '用于获取当前位置信息'},
{'type': Permission.locationAlways, 'name': '后台位置', 'description': '用于后台获取位置信息'}
]
},
{
'name': '媒体权限',
'icon': Icons.mic,
'permissions': [
{'type': Permission.microphone, 'name': '麦克风', 'description': '用于语音识别和录音'},
{'type': Permission.bluetooth, 'name': '蓝牙', 'description': '用于连接蓝牙设备'}
]
}
];
// 检查单个权限状态
Future<PermissionStatus> checkPermission(Permission permission) async {
return await permission.status;
}
// 请求单个权限
Future<PermissionStatus> requestPermission(Permission permission) async {
return await permission.request();
}
// 打开应用设置
void openSettings() {
openAppSettings();
}
// 检查所有权限状态并返回结果
Future<Map<Permission, PermissionStatus>> checkAllPermissions() async {
Map<Permission, PermissionStatus> statuses = {};
for (var group in permissionGroups) {
for (var perm in group['permissions']) {
Permission permission = perm['type'];
statuses[permission] = await permission.status;
}
}
return statuses;
}
@override
void onInit() {
super.onInit();

324
lib/modules/settings/views/permissions_view.dart

@ -0,0 +1,324 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import '../controllers/settings_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui';
class PermissionsView extends GetView<SettingsController> {
const PermissionsView({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,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
size: 20.w,
color: isDarkMode ? Colors.white : Colors.black87,
),
onPressed: () => Get.back(),
),
title: Text(
'权限管理'.tr,
style: TextStyle(
fontSize: 17.sp,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black,
),
),
centerTitle: true,
),
body: FutureBuilder<Map<Permission, PermissionStatus>>(
future: controller.checkAllPermissions(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(child: Text('获取权限状态失败: ${snapshot.error}'.tr));
}
final permissionStatuses = snapshot.data ?? {};
return ListView(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [
// 通讯权限组
_buildPermissionSection('通讯权限', Icons.phone_android, [
Permission.phone,
Permission.sms
], permissionStatuses, isDarkMode),
SizedBox(height: 20.h),
// 日历权限组
_buildPermissionSection('日历权限', Icons.calendar_today, [
Permission.calendarFullAccess
], permissionStatuses, isDarkMode),
SizedBox(height: 20.h),
// 位置权限组
_buildPermissionSection('位置权限', Icons.location_on, [
Permission.location
], permissionStatuses, isDarkMode),
],
);
},
),
);
}
// 构建权限组区域
Widget _buildPermissionSection(String title, IconData icon, List<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 组标题
Padding(
padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
child: Text(
title.tr,
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.blue[400] : Colors.blue[600],
letterSpacing: 1,
),
),
),
// 权限项容器
Container(
decoration: BoxDecoration(
color: isDarkMode ? Colors.white.withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isDarkMode
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16.r),
child: BackdropFilter(
filter: isDarkMode
? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Column(
children: _buildPermissionItems(permissions, statuses, isDarkMode),
),
),
),
),
],
);
}
// 构建权限项列表
List<Widget> _buildPermissionItems(List<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
final List<Widget> items = [];
// 获取描述文本
String getDescription(Permission permission) {
switch (permission) {
case Permission.phone:
return '用于拨打电话功能';
case Permission.sms:
return '用于发送短信功能';
case Permission.calendarWriteOnly:
return '用于创建日历事件和提醒';
case Permission.calendarFullAccess:
return '用于读取和管理日历事件及提醒';
case Permission.location:
return '用于获取当前位置信息';
default:
return '应用功能需要';
}
}
// 获取名称
String getName(Permission permission) {
switch (permission) {
case Permission.phone:
return '电话';
case Permission.sms:
return '短信';
case Permission.calendarWriteOnly:
return '日历写入';
case Permission.calendarFullAccess:
return '日历完整访问';
case Permission.location:
return '位置信息';
default:
return permission.toString();
}
}
for (int i = 0; i < permissions.length; i++) {
final permission = permissions[i];
final status = statuses[permission] ?? PermissionStatus.denied;
items.add(
_buildPermissionItem(
name: getName(permission),
description: getDescription(permission),
status: status,
permission: permission,
isDarkMode: isDarkMode,
),
);
// 添加分隔线,最后一个不加
if (i < permissions.length - 1) {
items.add(Divider(
height: 1,
color: isDarkMode ? Colors.white.withOpacity(0.1) : Colors.grey[200],
));
}
}
return items;
}
// 构建单个权限项
Widget _buildPermissionItem({
required String name,
required String description,
required PermissionStatus status,
required Permission permission,
required bool isDarkMode,
}) {
return Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name.tr,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
),
SizedBox(height: 4.h),
Text(
description.tr,
style: TextStyle(
fontSize: 12.sp,
color: isDarkMode ? Colors.white70 : Colors.grey[600],
),
),
SizedBox(height: 4.h),
_buildStatusText(status, isDarkMode),
],
),
),
Switch(
value: status.isGranted,
onChanged: (value) {
if (value) {
_handlePermissionAction(permission, status);
} else if (status.isGranted) {
controller.openSettings();
}
},
activeColor: Colors.blue[600],
),
],
),
);
}
// 状态文本
Widget _buildStatusText(PermissionStatus status, bool isDarkMode) {
String text;
Color color;
if (status.isGranted) {
text = '已授权'.tr;
color = Colors.green;
} else if (status.isDenied) {
text = '未授权'.tr;
color = Colors.red;
} else if (status.isPermanentlyDenied) {
text = '需要在系统设置中开启'.tr;
color = Colors.grey;
} else if (status.isLimited) {
text = '部分授权'.tr;
color = Colors.orange;
} else {
text = '未知状态'.tr;
color = Colors.grey;
}
return Text(
text,
style: TextStyle(
color: color,
fontSize: 12.sp,
fontWeight: FontWeight.w500,
),
);
}
// 处理权限请求
Future<void> _handlePermissionAction(Permission permission, PermissionStatus status) async {
if (status.isPermanentlyDenied) {
// 打开应用设置
controller.openSettings();
return;
}
if (!status.isGranted) {
// 请求权限
final result = await controller.requestPermission(permission);
if (result.isGranted) {
Get.snackbar('成功'.tr, '权限已授予'.tr, snackPosition: SnackPosition.BOTTOM);
} else if (result.isPermanentlyDenied) {
Get.dialog(
AlertDialog(
title: Text('需要权限'.tr),
content: Text('此权限需要在设置中手动开启'.tr),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消'.tr),
),
TextButton(
onPressed: () {
Get.back();
controller.openSettings();
},
child: Text('去设置'.tr),
),
],
),
);
}
// 刷新界面
Get.forceAppUpdate();
}
}
}

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

@ -355,7 +355,7 @@ class SettingsView extends GetView<SettingsController> {
Padding(
padding: EdgeInsets.only(left: 4.w, bottom: 8.h),
child: Text(
'systemSetting'.tr, // 系统设置
'systemSettings'.tr, // 系统设置
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
@ -586,6 +586,22 @@ class SettingsView extends GetView<SettingsController> {
// ? Colors.white.withOpacity(0.1)
// : Colors.grey[200]),
// 添加权限管理设置项
_buildSettingItem(
icon: Icons.security,
iconColor: Colors.orange,
title: '权限管理'.tr,
isDarkMode: isDarkMode,
onTap: () {
Get.toNamed(Routes.permissions);
},
),
Divider(
height: 1,
color: isDarkMode
? Colors.white.withOpacity(0.1)
: Colors.grey[200]),
// 退出登录
// _buildSimpleNavigationSetting(
// title: 'logout'.tr, // 退出登录
@ -1518,4 +1534,78 @@ class SettingsView extends GetView<SettingsController> {
),
);
}
// 添加权限管理设置项
Widget _buildSettingItem({
required IconData icon,
required Color iconColor,
required String title,
required bool isDarkMode,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.05)
: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
icon,
size: 16.w,
color: iconColor,
),
),
SizedBox(width: 12.w),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w500,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
Container(
width: 32.w,
height: 32.w,
decoration: BoxDecoration(
color: isDarkMode
? Colors.white.withOpacity(0.05)
: Colors.grey[100],
borderRadius: BorderRadius.circular(16.r),
),
child: Icon(
Icons.chevron_right,
size: 16.w,
color: isDarkMode ? Colors.white70 : Colors.grey[500],
),
),
],
),
),
);
}
}

18
lib/modules/speech_test/controllers/speech_test_controller.dart

@ -83,7 +83,7 @@ class SpeechTestController extends GetxController {
} catch (e) {
ttsStatus.value = '初始化失败';
recognitionStatus.value = '初始化失败';
Logger.error('语音测试初始化失败: $e');
Logger.error('语音测试初始化失败: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -198,7 +198,7 @@ class SpeechTestController extends GetxController {
// 重新初始化服务
await _initServices();
} catch (e) {
Logger.error('切换语音服务类型失败: $e');
Logger.error('切换语音服务类型失败: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -242,7 +242,7 @@ class SpeechTestController extends GetxController {
}
} catch (e) {
ttsStatus.value = '播放异常: $e';
Logger.error('单次播放异常: $e');
Logger.error('单次播放异常: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -270,7 +270,7 @@ class SpeechTestController extends GetxController {
await _ttsService.flushStream();
} catch (e) {
ttsStatus.value = '连续播放异常: $e';
Logger.error('连续播放异常: $e');
Logger.error('连续播放异常: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -284,7 +284,7 @@ class SpeechTestController extends GetxController {
await _ttsService.stop();
} catch (e) {
ttsStatus.value = '停止播放异常: $e';
Logger.error('停止播放异常: $e');
Logger.error('停止播放异常: ${e.toString()}');
}
}
@ -324,7 +324,7 @@ class SpeechTestController extends GetxController {
}
} catch (e) {
recognitionStatus.value = '识别异常: $e';
Logger.error('一次性识别异常: $e');
Logger.error('一次性识别异常: ${e.toString()}');
} finally {
isProcessing.value = false;
}
@ -351,7 +351,7 @@ class SpeechTestController extends GetxController {
_setupAsrEventListener(stream);
} catch (e) {
recognitionStatus.value = '启动连续识别异常: $e';
Logger.error('启动连续识别异常: $e');
Logger.error('启动连续识别异常: ${e.toString()}');
isListening.value = false;
intermediateResult.value = '';
finalResult.value = '';
@ -377,7 +377,7 @@ class SpeechTestController extends GetxController {
intermediateResult.value = '';
} catch (e) {
recognitionStatus.value = '停止连续识别异常: $e';
Logger.error('停止连续识别异常: $e');
Logger.error('停止连续识别异常: ${e.toString()}');
}
}
@ -413,4 +413,4 @@ class SpeechTestController extends GetxController {
super.onClose();
}
}
}

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

@ -1,5 +1,4 @@
import 'dart:async';
import 'package:amap_flutter/amap_flutter.dart';
import 'package:deep_voice/data/models/appconfig.dart';
import 'package:deep_voice/data/models/appconfig_model.dart';
import 'package:flutter/material.dart';
@ -32,21 +31,21 @@ class SplashController extends GetxController {
WidgetsBinding.instance.addPostFrameCallback((_) {
_startLoadingAnimation();
_loadAppResources();
_initMapSDK();
// _initMapSDK();
});
}
// 初始化地图SDK
void _initMapSDK() {
try {
AmapFlutter().initSdk(
iosKey: "a4a1394fe817c2f86a424b897b4a9af4",
androidKey: "e5fdc9605eabdeb5626f18f5721f343d",
);
} catch (e) {
print('地图SDK初始化失败: $e');
}
}
// void _initMapSDK() {
// try {
// AmapFlutter().initSdk(
// iosKey: "a4a1394fe817c2f86a424b897b4a9af4",
// androidKey: "e5fdc9605eabdeb5626f18f5721f343d",
// );
// } catch (e) {
// print('地图SDK初始化失败: $e');
// }
// }
// 进度条动画(持续2秒)
void _startLoadingAnimation() {

25
lib/modules/translation/controllers/translation_controller.dart

@ -51,6 +51,7 @@ class TranslationController extends GetxController {
currentMode.value = mode;
currentModeTitle.value = _getModeTitle(mode);
// 这里添加具体模式切换逻辑
stopAll();
}
String _getModeTitle(String mode) {
@ -175,7 +176,7 @@ class TranslationController extends GetxController {
Logger.info('所有服务初始化完成');
} catch (e) {
Logger.error('初始化服务失败: $e');
Logger.error('初始化服务失败: ${e.toString()}');
}
}
@ -252,7 +253,7 @@ class TranslationController extends GetxController {
recognitionStream.listen(_handleRecognitionEvent);
} catch (e) {
isRecognizing.value = false;
Logger.error('启动语音识别失败: $e');
Logger.error('启动语音识别失败: ${e.toString()}');
}
}
@ -307,7 +308,7 @@ class TranslationController extends GetxController {
currentSessionId = null;
} catch (e) {
Logger.error('停止语音识别失败: $e');
Logger.error('停止语音识别失败: ${e.toString()}');
}
}
@ -445,7 +446,7 @@ class TranslationController extends GetxController {
}
}
} catch (e) {
Logger.error('翻译失败: $e');
Logger.error('翻译失败: ${e.toString()}');
} finally {
isTranslating.value = false;
}
@ -468,7 +469,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text);
} catch (e) {
Logger.error('播放翻译失败: $e');
Logger.error('播放翻译失败: ${e.toString()}');
}
}
@ -482,7 +483,7 @@ class TranslationController extends GetxController {
_translationDebounceTimer?.cancel();
await _ttsService.stop();
} catch (e) {
Logger.error('停止操作失败: $e');
Logger.error('停止操作失败: ${e.toString()}');
}
}
@ -497,7 +498,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(item.translatedText);
} catch (e) {
Logger.error('播放翻译项失败: $e');
Logger.error('播放翻译项失败: ${e.toString()}');
}
}
@ -516,7 +517,7 @@ class TranslationController extends GetxController {
await _ttsService.setVoice(voiceName);
await _ttsService.speakOnce(text);
} catch (e) {
Logger.error('播放文本失败: $e');
Logger.error('播放文本失败: ${e.toString()}');
}
}
@ -531,7 +532,7 @@ class TranslationController extends GetxController {
curve: Curves.easeOut,
);
} catch (e) {
Logger.error('滚动到底部失败: $e');
Logger.error('滚动到底部失败: ${e.toString()}');
}
}
}
@ -629,7 +630,7 @@ class TranslationController extends GetxController {
Logger.info('ASR服务重新初始化成功');
} catch (e) {
Logger.error('重新初始化ASR服务失败: $e');
Logger.error('重新初始化ASR服务失败: ${e.toString()}');
}
}
@ -656,7 +657,7 @@ class TranslationController extends GetxController {
});
} catch (e) {
_storage.remove(_historyKey);
Logger.error('加载翻译历史失败: $e');
Logger.error('加载翻译历史失败: ${e.toString()}');
}
}
}
@ -673,7 +674,7 @@ class TranslationController extends GetxController {
translationHistory.map((item) => item.toJson()).toList();
_storage.write(_historyKey, jsonEncode(historyJson));
} catch (e) {
Logger.error('保存翻译历史失败: $e');
Logger.error('保存翻译历史失败: ${e.toString()}');
}
}

8
lib/modules/translation/controllers/translation_detail_controller.dart

@ -58,7 +58,7 @@ class TranslationDetailController extends GetxController {
loadSavedSummary();
});
} catch (e) {
Logger.error('初始化详情控制器失败: $e');
Logger.error('初始化详情控制器失败: ${e.toString()}');
} finally {
isLoading.value = false;
}
@ -72,7 +72,7 @@ class TranslationDetailController extends GetxController {
summaryResult.value = savedSummary;
}
} catch (e) {
Logger.error('加载摘要失败: $e');
Logger.error('加载摘要失败: ${e.toString()}');
}
}
@ -83,7 +83,7 @@ class TranslationDetailController extends GetxController {
try {
_storage.write(_getSummaryKey(), summary);
} catch (e) {
Logger.error('保存摘要失败: $e');
Logger.error('保存摘要失败: ${e.toString()}');
}
}
@ -174,7 +174,7 @@ class TranslationDetailController extends GetxController {
return result;
} catch (e) {
final errorMessage = '生成摘要失败: ${e.toString()}';
Logger.error(errorMessage);
Logger.error('TranslationDetailController', errorMessage);
summaryResult.value = errorMessage;
return errorMessage;
} finally {

25
lib/routes/app_pages.dart

@ -1,3 +1,4 @@
import 'package:deep_voice/modules/navigation/views/navigation_view.dart';
import 'package:get/get.dart';
import '../modules/feedback/bindings/feedback_bindings.dart';
import '../modules/feedback/views/feedback_view.dart';
@ -28,9 +29,8 @@ import '../modules/pairing/views/pairing_view.dart';
import '../modules/pairing/bindings/pairing_binding.dart';
import './app_routes.dart';
import '../modules/settings/views/settings_view.dart';
import '../modules/settings/views/permissions_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';
import '../modules/opus_test/views/opus_test_view.dart';
import '../modules/opus_test/bindings/opus_test_binding.dart';
import '../modules/speech_test/views/speech_test_view.dart';
@ -162,13 +162,6 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.jieliTest,
page: () => const JieliTestView(),
binding: JieliTestBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.opusTest,
page: () => const OpusTestView(),
@ -190,5 +183,19 @@ abstract class AppPages {
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.permissions,
page: () => const PermissionsView(),
binding: SettingsBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
GetPage(
name: Routes.navigation,
page: () => const NavigationView(),
binding: AgentBinding(),
transition: Transition.rightToLeft,
transitionDuration: Duration(milliseconds: 250),
),
];
}

2
lib/routes/app_routes.dart

@ -16,6 +16,7 @@ abstract class Routes {
static const OTA_UPGRADE = '/ota_upgrade'; // OTA升级
static const timbre = '/timbre'; // 音色设置
static const settings = '/settings'; // 系统设置
static const permissions = '/permissions'; // 权限管理
static const recording = '/recording'; // 录音功能
// 会议相关
@ -43,4 +44,5 @@ abstract class Routes {
// 支持服务
static const feedback = '/feedback'; // 用户反馈
static const agent = '/agent'; // 在线客服
static const navigation = '/navigation'; // 实时导航
}

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

Loading…
Cancel
Save