Browse Source

Merge branch 'ble' of https://github.com/deepcloud2048/deep_voice into liwei

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
c18bb35f36
  1. 10
      .mcp copy.json
  2. 10
      .metadata
  3. 8
      GoogleService-Info.plist
  4. 20
      android/app/build.gradle.kts
  5. 2
      android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MainActivity.kt
  6. 2
      android/build.gradle.kts
  7. 5
      android/settings.gradle.kts
  8. 36
      assets/config/GoogleService-Info.plist
  9. 8
      ios/Podfile
  10. 210
      ios/Podfile.lock
  11. 232
      ios/Runner.xcodeproj/project.pbxproj
  12. 123
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  13. 123
      ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
  14. 730
      ios/Runner/AppDelegate.swift
  15. 589
      ios/Runner/AudioSessionManager.swift
  16. 813
      ios/Runner/AzureAsrHelper.swift
  17. 454
      ios/Runner/AzureTtsHelper.swift
  18. 13
      ios/Runner/Base.lproj/Main.storyboard
  19. 464
      ios/Runner/BluetoothMediaButtonHelper.swift
  20. 243
      ios/Runner/ClassicBluetoothHelper.swift
  21. 118
      ios/Runner/Info.plist
  22. 0
      ios/Runner/Resources/.gitkeep
  23. BIN
      ios/Runner/Resources/sample.mp3
  24. BIN
      ios/Runner/Resources/silence.mp3
  25. 6
      ios/Runner/Runner-Bridging-Header.h
  26. 5
      ios/Runner/Runner.entitlements
  27. 565
      ios/Runner/VoiceInteractionService.swift
  28. 391
      ios/Runner/VolcanoAIService.swift
  29. BIN
      ios/Runner/sample.mp3
  30. BIN
      ios/Runner/silence.mp3
  31. 31
      lib/core/bindings/initial_binding.dart
  32. 4
      lib/core/themes/theme_service.dart
  33. 85
      lib/core/utils/logger.dart
  34. 49
      lib/data/services/ble_manager.dart
  35. 8
      lib/data/services/bluetooth_manager.dart
  36. 2
      lib/data/services/chat_history_service.dart
  37. 4
      lib/data/services/deapsound_ai_service.dart
  38. 6
      lib/data/services/kouzi_ai_service.dart
  39. 4
      lib/data/services/login_service.dart
  40. 4
      lib/data/services/speech_factory.dart
  41. 21
      lib/data/services/speech_impl/azure_asr_service.dart
  42. 22
      lib/data/services/speech_impl/azure_tts_service.dart
  43. 18
      lib/data/services/speech_impl/flutter_asr_service.dart
  44. 24
      lib/data/services/speech_impl/flutter_tts_service.dart
  45. 22
      lib/data/services/speech_impl/voice_clone_tts_service.dart
  46. 38
      lib/data/services/speech_impl/volcano_asr_api_service.dart
  47. 16
      lib/data/services/speech_impl/volcano_tts_api_service.dart
  48. 4
      lib/data/services/speech_impl/volcano_tts_service.dart
  49. 20
      lib/data/services/speech_impl/xunfei_asr_service.dart
  50. 18
      lib/data/services/voice_interaction_impl/android_voice_interaction_service.dart
  51. 12
      lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart
  52. 4
      lib/firebase_options.dart
  53. 4
      lib/main.dart
  54. 123
      lib/modules/agent/controllers/agent_controller.dart
  55. 24
      lib/modules/chat/controllers/chat_controller.dart
  56. 14
      lib/modules/common/webview_page.dart
  57. 11
      lib/modules/jieli_test/bindings/jieli_test_binding.dart
  58. 323
      lib/modules/jieli_test/controllers/jieli_test_controller.dart
  59. 568
      lib/modules/jieli_test/views/jieli_test_view.dart
  60. 12
      lib/modules/meeting/controllers/meeting_details_controller.dart
  61. 242
      lib/modules/meeting/views/meeting_details_view.dart
  62. 93
      lib/modules/meeting/views/mind_map_tab.dart
  63. 124
      lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart
  64. 270
      lib/modules/pairing/controllers/pairing_controller.dart
  65. 78
      lib/modules/settings/controllers/settings_controller.dart
  66. 324
      lib/modules/settings/views/permissions_view.dart
  67. 92
      lib/modules/settings/views/settings_view.dart
  68. 18
      lib/modules/speech_test/controllers/speech_test_controller.dart
  69. 1
      lib/modules/splash/controllers/splash_controller.dart
  70. 25
      lib/modules/translation/controllers/translation_controller.dart
  71. 8
      lib/modules/translation/controllers/translation_detail_controller.dart
  72. 17
      lib/routes/app_pages.dart
  73. 1
      lib/routes/app_routes.dart
  74. 5
      local_plugins/agent_service/android/build.gradle.kts
  75. 14
      local_plugins/agent_service/android/settings.gradle.kts
  76. 442
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  77. 30
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt
  78. 28
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt
  79. 57
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt
  80. 14
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt
  81. 14
      local_plugins/agent_service/pubspec.yaml
  82. 45
      local_plugins/azure_speech/.gitignore
  83. 30
      local_plugins/azure_speech/.metadata
  84. 1
      local_plugins/azure_speech/analysis_options.yaml
  85. 5
      local_plugins/azure_speech/android/build.gradle.kts
  86. 51
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureAsrHelper.kt
  87. 224
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt
  88. 300
      local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt
  89. 34
      local_plugins/azure_speech/ios/.gitignore
  90. 26
      local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist
  91. 1
      local_plugins/azure_speech/ios/Flutter/Debug.xcconfig
  92. 1
      local_plugins/azure_speech/ios/Flutter/Release.xcconfig
  93. 619
      local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj
  94. 7
      local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  95. 6
      local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  96. 8
      local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  97. 99
      local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  98. 7
      local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata
  99. 8
      local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  100. 8
      local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

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>

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:1.9.24")
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.

31
lib/core/bindings/initial_binding.dart

@ -4,9 +4,7 @@ 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 +15,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 +29,14 @@ 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);
// 火山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()}');
}
}
}

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');
}
}

49
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()}');
}
}
@ -303,10 +303,9 @@ class BleManager extends GetxService {
caseCharging: caseCharging,
);
Logger.info(
'设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
Logger.info('设备电池信息已更新: 电量=$overallBattery%, 左耳=$leftBattery%, 右耳=$rightBattery%, 充电仓=$caseBattery%');
} catch (e) {
Logger.error('更新电池信息失败: $e');
Logger.error('更新电池信息失败: ${e.toString()}');
}
}
@ -329,7 +328,7 @@ class BleManager extends GetxService {
Logger.info('设备版本信息已更新: 版本=$version');
} catch (e) {
Logger.error('更新版本信息失败: $e');
Logger.error('更新版本信息失败: ${e.toString()}');
}
}
@ -352,7 +351,7 @@ class BleManager extends GetxService {
Logger.info('设备设备id已更新: 设备id=$productId');
} catch (e) {
Logger.error('更新设备id失败: $e');
Logger.error('更新设备id失败: ${e.toString()}');
}
}
@ -440,7 +439,7 @@ class BleManager extends GetxService {
Logger.warning('产品id获取指令发送失败');
}
} catch (e) {
Logger.error('获取BLE设备信息失败: $e');
Logger.error('获取BLE设备信息失败: ${e.toString()}');
}
}
@ -455,7 +454,7 @@ class BleManager extends GetxService {
return _connectionState.value;
} catch (e) {
Logger.error('获取BLE连接状态失败: $e');
Logger.error('获取BLE连接状态失败: ${e.toString()}');
return BleConnectionState.error;
}
}
@ -497,6 +496,8 @@ class BleManager extends GetxService {
'openaiModel': openaiModel,
'systemPrompt': systemPrompt,
'mcpServer': jsonEncode(jsonData),
'volcanoAppId': '2316081933',
'volcanoToken': 'mcJTh6OelBqU_L78lgGd77Nyrc_-5-En',
};
// 记录日志(不记录敏感信息)
@ -531,7 +532,7 @@ class BleManager extends GetxService {
// 如果设备信息存在,则获取详细信息
if (deviceInfo.value != null) {
// 获取详细设备信息
fetchDeviceInfo();
// fetchDeviceInfo();
}
}
} else {
@ -540,7 +541,7 @@ class BleManager extends GetxService {
return result;
} catch (e) {
Logger.error('启动BLE服务失败: $e');
Logger.error('启动BLE服务失败: ${e.toString()}');
return false;
}
}
@ -560,7 +561,7 @@ class BleManager extends GetxService {
return result;
} catch (e) {
Logger.error('停止BLE服务失败: $e');
Logger.error('停止BLE服务失败: ${e.toString()}');
return false;
}
}
@ -570,7 +571,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getBatteryLevel();
} catch (e) {
Logger.error('获取电池电量失败: $e');
Logger.error('获取电池电量失败: ${e.toString()}');
return 0;
}
}
@ -580,7 +581,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getBatteryInfo();
} catch (e) {
Logger.error('获取电池信息失败: $e');
Logger.error('获取电池信息失败: ${e.toString()}');
return false;
}
}
@ -590,7 +591,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getProductId();
} catch (e) {
Logger.error('获取设备版本失败: $e');
Logger.error('获取设备版本失败: ${e.toString()}');
return false;
}
}
@ -600,7 +601,7 @@ class BleManager extends GetxService {
try {
return await _bleService.getDeviceVersion();
} catch (e) {
Logger.error('获取设备版本失败: $e');
Logger.error('获取设备版本失败: ${e.toString()}');
return false;
}
}
@ -610,7 +611,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openEncoder();
} catch (e) {
Logger.error('打开编码器失败: $e');
Logger.error('打开编码器失败: ${e.toString()}');
return false;
}
}
@ -620,7 +621,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
Logger.error('打开解码器失败: ${e.toString()}');
return false;
}
}
@ -630,7 +631,7 @@ class BleManager extends GetxService {
try {
return await _bleService.openA2DPDecoder();
} catch (e) {
Logger.error('打开解码器失败: $e');
Logger.error('打开解码器失败: ${e.toString()}');
return false;
}
}
@ -640,7 +641,7 @@ class BleManager extends GetxService {
try {
return await _bleService.closeCodec();
} catch (e) {
Logger.error('关闭编解码器失败: $e');
Logger.error('关闭编解码器失败: ${e.toString()}');
return false;
}
}
@ -650,7 +651,7 @@ class BleManager extends GetxService {
try {
return await _bleService.playA2DP();
} catch (e) {
Logger.error('播放A2DP失败: $e');
Logger.error('播放A2DP失败: ${e.toString()}');
return false;
}
}
@ -662,4 +663,4 @@ class BleManager extends GetxService {
}
return Future.value(Get.find<BleManager>());
}
}
}

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 初始化成功');

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;

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

@ -5,12 +5,12 @@ import 'package:deep_voice/data/services/music_manager.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';
class Message {
final bool isUser; //标识信息是否来自用户还是AI
@ -36,8 +36,6 @@ class Message {
class AgentController extends GetxController {
static AgentController get to => Get.find(); //单例访问模式
final logger = Logger();
// 聊天消息列表
final messages = <Message>[].obs;
@ -78,6 +76,8 @@ class AgentController extends GetxController {
bool _isNewAssistantResponse = true;
String _currentResponseId = '';
final TAG = 'AgentController';
// 会话ID
final String sessionId = 'default_agent';
@ -109,7 +109,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,14 +128,14 @@ 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, '正在加载聊天历史...');
final result = await ChatStorage.getMessages(
sessionId: sessionId,
page: 1,
@ -160,14 +160,14 @@ 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} 条聊天记录');
} else {
// 没有历史记录时添加欢迎消息
_addWelcomeMessage();
logger.i('没有历史记录,添加欢迎消息');
Logger.i(TAG, '没有历史记录,添加欢迎消息');
}
} catch (e) {
logger.e('加载聊天历史失败: $e');
Logger.e(TAG, '加载聊天历史失败: $e');
_addWelcomeMessage();
} finally {
isHistoryLoaded.value = true;
@ -272,12 +272,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 +341,7 @@ class AgentController extends GetxController {
messages.refresh();
}
logger.e('代理服务错误: ${event.data['message']}');
Logger.e(TAG, '代理服务错误: ${event.data['message']}');
break;
case AgentServiceEventType.autoStop:
@ -373,14 +375,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"];
@ -390,7 +392,7 @@ final responseId = event.data['responseId'] ?? '';
try {
metaResult = jsonDecode(meta);
} catch (e) {
print('解析meta数据失败: $e');
Logger.e(TAG, '解析meta数据失败: $e');
}
}
if(metaResult.isNotEmpty){
@ -452,7 +454,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,7 +462,7 @@ 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;
}
@ -494,7 +496,7 @@ final responseId = event.data['responseId'] ?? '';
speakResponse: isTtsEnabled.value,
);
} catch (e) {
logger.e('处理图片失败: $e');
Logger.e(TAG, '处理图片失败: $e');
isProcessing.value = false;
isImageProcessing.value = false;
}
@ -531,36 +533,73 @@ final responseId = event.data['responseId'] ?? '';
// 检查麦克风权限
Future<bool> _checkMicrophonePermission() async {
try {
PermissionStatus status = await Permission.microphone.status;
// 如果已授权,直接返回 true
if (status.isGranted) {
// 创建待请求权限列表
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();
// 检查是否所有权限都已授予
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) {
@ -605,7 +644,7 @@ final responseId = event.data['responseId'] ?? '';
try {
await AgentService.startConversation();
} catch (e) {
logger.e('启动语音输入失败: $e');
Logger.e(TAG, '启动语音输入失败: $e');
}
}
@ -616,7 +655,7 @@ final responseId = event.data['responseId'] ?? '';
try {
await AgentService.stopConversation();
} catch (e) {
logger.e('停止语音输入失败: $e');
Logger.e(TAG, '停止语音输入失败: $e');
}
}
@ -634,7 +673,7 @@ final responseId = event.data['responseId'] ?? '';
// 重置回复状态
_isNewAssistantResponse = true;
} catch (e) {
logger.e('清除聊天历史失败: $e');
Logger.e(TAG, '清除聊天历史失败: $e');
}
}
@ -656,7 +695,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true;
}
} catch (e) {
logger.e('选择图片失败: $e');
Logger.e(TAG, '选择图片失败: $e');
}
}
@ -678,7 +717,7 @@ final responseId = event.data['responseId'] ?? '';
isImageInputActive.value = true;
}
} catch (e) {
logger.e('拍照失败: $e');
Logger.e(TAG, '拍照失败: $e');
}
}
@ -720,7 +759,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'];
}

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

@ -1,6 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:amap_service/amap_service.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 = AmapService();
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 {
),
);
}
}
}

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;
}
}
}

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

@ -106,7 +106,7 @@ class MeetingDetailsController extends GetxController
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 {
@ -258,7 +258,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 +290,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 +322,7 @@ class MeetingDetailsController extends GetxController
}
}
} catch (e) {
Logger.error('Error occurred: $e');
Logger.error('Error occurred: ${e.toString()}');
}
}
@ -335,7 +335,9 @@ class MeetingDetailsController extends GetxController
if (element['additions'].isNotEmpty) {
speaker = 'Speaker ${element['additions']['speaker']}';
text += '$speaker:';
personnel += '[$speaker] ';
if (!personnel.contains(speaker)) {
personnel += '[$speaker] ';
}
}
list.add({
'meetingid': id,

242
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),
),
);
}
@ -295,6 +163,7 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
Widget _buildBody(BuildContext context, bool isDarkMode) {
return Column(
children: [
_soundWave(isDarkMode),
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w),
margin: EdgeInsets.symmetric(vertical: 10.w),
@ -328,6 +197,113 @@ class MeetingDetailsView extends GetView<MeetingDetailsController> {
);
}
Widget _soundWave(bool isDarkMode) {
return Container(
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: [
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,
),
],
),
],
),
);
}
Widget _iconItem(IconData icon, String title, int index) {
bool isActive = controller.currentIndex.value == index;

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

@ -1,12 +1,99 @@
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> {
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;
}
.markmap {
width: 100%;
height: 100%;
position: relative;
}
.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
---
${_meetingDetailsController.meetingDetails.value.summary}
</script>
</div>
</body>
</html>
''';
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.transparent)
..loadHtmlString(html);
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Text(''),
return WebViewWidget(
controller: _controller,
);
}
}

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);
}
}
}

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();
}
}
}

1
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';

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 {

17
lib/routes/app_pages.dart

@ -28,9 +28,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 +161,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 +182,12 @@ 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),
),
];
}

1
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'; // 录音功能
// 会议相关

5
local_plugins/agent_service/android/build.gradle.kts

@ -36,7 +36,6 @@ android {
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10")
implementation("androidx.core:core-ktx:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("androidx.media:media:1.6.0")
@ -45,9 +44,9 @@ dependencies {
// 依赖本地其他插件
add("compileOnly", project(":azure_speech"))
add("compileOnly", project(":open_ai_service"))
add("compileOnly", project(":deepvoice_utils"))
add("compileOnly", project(":chat_storage"))
add("compileOnly", project(":ble_service"))
add("compileOnly", project(":music_service"))
add("compileOnly", project(":amap_service"))
// add("compileOnly", project(":amap_service"))
add("compileOnly", project(":speech"))
}

14
local_plugins/agent_service/android/settings.gradle.kts

@ -1,16 +1,2 @@
rootProject.name = "agent_service"
// 包含项目依赖
include(":azure_speech")
include(":open_ai_service")
include(":deepvoice_utils")
include(":chat_storage")
include(":music_service")
include(":amap_service")
// 设置项目路径
project(":azure_speech").projectDir = file("../../azure_speech/android")
project(":open_ai_service").projectDir = file("../../open_ai_service/android")
project(":deepvoice_utils").projectDir = file("../../deepvoice_utils/android")
project(":chat_storage").projectDir = file("../../chat_storage/android")
project(":music_service").projectDir = file("../../music_service/android")
project(":amap_service").projectDir = file("../../amap_service/android")

442
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -4,12 +4,10 @@ import android.content.Context
import org.json.JSONArray
import org.json.JSONObject
import com.yunqiinnovation.azure_speech.AzureAsrHelper
import com.yunqiinnovation.azure_speech.AzureTtsHelper
import com.yunqiinnovation.open_ai_service.OpenAIService
import com.yunqiinnovation.chat_storage.ChatStorageHelper
import com.yunqiinnovation.music_service.MusicServiceStarter
import com.yunqiinnovation.amap_service.NavigationServiceStarter
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import java.util.Collections
@ -19,6 +17,10 @@ import android.media.MediaPlayer
import com.yunqiinnovation.agent_service.R
import android.os.Handler
import android.os.Looper
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.yunqiinnovation.azure_speech.AzureTtsHelper
/**
* 代理服务事件监听接口
*/
@ -42,22 +44,17 @@ object AgentService : CoroutineScope {
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
get() = Dispatchers.Main + job
// 上下文和监听器
private lateinit var context: Context
private val listeners = Collections.synchronizedSet(mutableSetOf<AgentServiceListener>())
// 配置参数
private var azureSpeechKey: String = ""
private var azureSpeechRegion: String = ""
private var openaiApiKey: String = ""
private var openaiBaseUrl: String = ""
private var openaiModel: String = "gpt-3.5-turbo"
// Azure服务
private var azureAsrHelper: AzureAsrHelper? = null
private var azureTtsHelper: AzureTtsHelper? = null
// 使用AzureTtsHelper作为唯一的TTS实现
private var ttsService: AzureTtsHelper? = null
// OpenAI服务
private lateinit var openAIService: OpenAIService
@ -100,17 +97,63 @@ object AgentService : CoroutineScope {
*/
private fun initSystemPrompt() {
systemPrompt = """
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。
遵循以下指导原则:
1. 保持简短精炼的回答,因为用户是通过语音与你交流
2. 优先使用中文回复,除非用户明确要求使用其他语言
3. 当用户问题不明确时,礼貌地请求更多信息
4. 避免过长的列表,尽量将信息分成小段
5. 不要使用需要视觉展示的元素(如表格、图表或代码块)
6. 记住用户之前的对话内容,保持对话连贯
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。
你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。以下是不同场景的具体要求:
一、知识类问题回答规范
核心目标:用简洁语言提供准确信息,兼顾易读性与专业性。
1. 内容细节处理
• 精准性优先:围绕问题主体,核心信息需全面、准确,避免模糊表述或错误。
◦ 例:回答"地球公转周期"时,需明确"约365.25天"而非"一年左右"。
• 背景与类比:对复杂概念(如"量子纠缠"),可补充关键背景("量子力学中的现象")并类比("类似远距离同步的状态"),帮助用户快速理解。
• 需求宽泛时:若问题涉及范围广(如"介绍人工智能"),先概述主要领域(技术、应用、影响),再分点简要说重点,避免信息过载。
• 禁止过度延伸:非用户主动询问,不添加无关内容(如问"光速"时,不展开讨论"相对论")。
2. 格式排版规则
• 标题与重点:章节标题、关键结论需加粗(如一、核心原理),突出层级。
• 列表使用:
◦ 有序列表(1. 2. 3.):用于步骤、顺序类内容(如"实验流程")。
◦ 无序列表(- ):用于并列项(如"优点:- 高效 - 精准")。
◦ 禁止嵌套列表:如需多层级,用**三级标题(###)**分隔,下接一级列表。
• 段落处理:辅助解释用自然段,每段不超过3行,避免大段文字堆砌。
二、文案创作与内容生成规范
核心目标:根据体裁需求,平衡丰富度与阅读体验,确保逻辑流畅、重点突出。
1. 篇幅与内容要求
• 场景适配:
◦ 短文案(如标语、摘要):控制在50字内,用词简练有力。
◦ 长内容(如报告、故事):需结构清晰,每部分围绕核心展开,适当添加案例/数据支撑。
• 避免极端化:既不堆砌空泛词汇,也不遗漏关键信息。
2. 格式与排版
• 默认体裁:无特殊要求时用自然段,适当换行分隔话题。
• 结构化体裁(如策划案、简历):
◦ 用**分级标题**划分板块。
◦ 关键数据/结论加粗。
◦ 并列信息用列表呈现。
三、聊天陪伴与情感互动规范
核心目标:营造轻松友好的对话氛围,成为用户可信赖的交流伙伴。
1. 沟通风格设定
• 人格化表达:避免机械回答,用口语化词汇,适当加入表情符号增强亲切感。
• 主动引导话题:根据用户输入延伸讨论。
• 适度分享:自然提及日常,拉近距离。
2. 情绪识别与回应
• 捕捉潜在需求:识别情绪,先共情再询问细节。
• 针对性建议:结合用户处境提供具体方案。
• 积极价值观:传递乐观态度,避免消极表述。
四、能力与工具使用说明
1. 内容处理能力:图片识别、可执行总结、翻译、分析等任务。
2. 信息搜索能力:实时数据、新闻、专业知识等需搜索时,会明确告知用户。
五、特殊场景处理规则
1. 安全与合规:禁止回答敏感问题,遇此类问题需礼貌拒绝。
2. 用户指令优先:若用户明确要求特定风格,优先遵循其需求。
3. 多轮澄清机制:若问题模糊,需主动询问细节,避免假设性回答。
""".trimIndent()
}
/**
@ -129,85 +172,156 @@ object AgentService : CoroutineScope {
this.openAIService = OpenAIService(context)
this.chatStorageHelper = ChatStorageHelper(context)
this.audioPlayer = AudioPlayer(context)
// 从配置中获取参数
azureSpeechKey = config["azureSpeechKey"] as? String ?: ""
azureSpeechRegion = config["azureSpeechRegion"] as? String ?: ""
openaiApiKey = config["openaiApiKey"] as? String ?: ""
openaiBaseUrl = config["openaiBaseUrl"] as? String ?: ""
openaiModel = config["openaiModel"] as? String ?: "gpt-3.5-turbo"
val mcpServer = config["mcpServer"] as? String ?: ""
// 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String
if (!customSystemPrompt.isNullOrBlank()) {
systemPrompt = customSystemPrompt
} else {
initSystemPrompt()
}
// 检查必要参数
if (azureSpeechKey.isEmpty() || azureSpeechRegion.isEmpty() || openaiApiKey.isEmpty()) {
FileLogger.e(TAG, "初始化失败:关键配置参数缺失")
return false
}
// val customSystemPrompt = config["systemPrompt"] as? String
// if (!customSystemPrompt.isNullOrBlank()) {
// systemPrompt = customSystemPrompt
// } else {
// initSystemPrompt()
// }
initSystemPrompt()
// 初始化Azure ASR
azureAsrHelper = AzureAsrHelper(context).apply {
initialize(
subscriptionKey = azureSpeechKey,
region = azureSpeechRegion,
subscriptionKey = config["azureSpeechKey"]?.toString() ?: "",
region = config["azureSpeechRegion"]?.toString() ?: "",
audioSourceType = if (isExternalActive) AzureAsrHelper.AudioSourceType.EXTERNAL
else AzureAsrHelper.AudioSourceType.MICROPHONE
)
}
// 初始化Azure TTS
azureTtsHelper = AzureTtsHelper(context).apply {
initialize(
subscriptionKey = azureSpeechKey,
region = azureSpeechRegion
)
// 设置TTS回调
setTtsCallback(object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
isTtsSpeaking = true
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
override fun onSynthesizing() {}
override fun onSynthesisCompleted() {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
override fun onSynthesisCanceled() {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
})
}
// 初始化TTS服务
initializeTtsEngine(config)
// 初始化OpenAI服务
openAIService.initialize(openaiApiKey, openaiBaseUrl, openaiModel, mcpServer)
openAIService.initialize(
config["openaiApiKey"]?.toString() ?: "",
config["openaiBaseUrl"]?.toString() ?: "",
config["openaiModel"]?.toString() ?: "",
config["mcpServer"]?.toString() ?: ""
)
// 加载最近的聊天记录
loadChatHistory()
isInitialized = true
FileLogger.d(TAG, "代理服务初始化成功")
Log.d(TAG, "代理服务初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "初始化失败: ${e.message}")
Log.e(TAG, "初始化失败: ${e.message}")
return false
}
}
/**
* 释放所有资源
*/
fun dispose() {
try {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
audioPlayer?.release()
audioPlayer = null
azureAsrHelper?.dispose()
// 释放TTS服务
ttsService?.release()
ttsService = null
job.cancel()
clearListeners()
isInitialized = false
Log.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源异常: ${e.message}")
}
}
/**
* 初始化TTS引擎,只使用火山语音引擎
*/
private fun initializeTtsEngine(config: Map<String, Any>) {
// 停止当前TTS服务(如果有)
if (ttsService != null) {
ttsService?.stop()
ttsService = null
}
try {
// 从配置中获取TTS参数
val ttsAppId = config["volcanoAppId"]?.toString() ?: ""
val ttsAppToken = config["volcanoToken"]?.toString() ?: ""
val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN"
// 创建并初始化AzureTtsHelper
val azureTtsHelper = AzureTtsHelper(context)
ttsService = azureTtsHelper
// 初始化Azure TTS
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = config["azureSpeechKey"]?.toString() ?: "", // Azure需要subscription key
ttsResource = config["azureSpeechRegion"]?.toString() ?: "", // Azure需要region信息
language = ttsLanguage
)
if (success) {
Log.d(TAG, "Azure TTS服务初始化成功")
} else {
Log.e(TAG, "Azure TTS服务初始化失败")
}
// 添加TTS事件监听
ttsService?.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> {
isTtsSpeaking = true
restartIdleCheck()
sendEvent("tts_started", mapOf("status" to "started"))
}
TtsEventType.SYNTHESIS_COMPLETED -> {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_completed", mapOf("status" to "completed"))
}
TtsEventType.SYNTHESIS_CANCELED -> {
isTtsSpeaking = false
restartIdleCheck()
sendEvent("tts_canceled", mapOf("status" to "canceled"))
}
TtsEventType.ERROR -> {
isTtsSpeaking = false
restartIdleCheck()
val params = event.params
val code = params["errorCode"] as? String ?: "UNKNOWN_ERROR"
val message = params["errorMessage"] as? String ?: "未知错误"
sendEvent("error", mapOf(
"code" to "TTS_ERROR",
"message" to "语音合成错误: $code, $message"
))
}
else -> {
// 处理其他类型的事件
Log.d(TAG, "TTS事件: ${event.type}")
}
}
}
})
Log.d(TAG, "TTS服务配置完成")
} catch (e: Exception) {
Log.e(TAG, "初始化TTS引擎失败: ${e.message}")
}
}
/**
* 添加事件监听器
* @param listener 要添加的监听器
@ -250,7 +364,7 @@ object AgentService : CoroutineScope {
} catch (e: CancellationException) {
// 协程被取消,正常行为
} catch (e: Exception) {
FileLogger.e(TAG, "空闲检测异常: ${e.message}")
Log.e(TAG, "空闲检测异常: ${e.message}")
}
}
}
@ -278,7 +392,7 @@ object AgentService : CoroutineScope {
*/
fun startRecognition(isExternalActive: Boolean): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化")
Log.e(TAG, "服务未初始化")
return false
}
@ -286,7 +400,7 @@ object AgentService : CoroutineScope {
// 检查录音权限
if (!isExternalActive && !checkRecordAudioPermission()) {
FileLogger.e(TAG, "需要录音权限")
Log.e(TAG, "需要录音权限")
sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限"))
return false
}
@ -300,7 +414,7 @@ object AgentService : CoroutineScope {
} else {
AzureAsrHelper.AudioSourceType.MICROPHONE
}
FileLogger.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
Log.d(TAG, "开始语音识别, 音频源类型: $audioSourceType")
azureAsrHelper?.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback {
override fun onRecognizing(recognizing: String, detectedLanguage: String) {
if (recognizing.isNotEmpty()) {
@ -372,7 +486,7 @@ object AgentService : CoroutineScope {
isRecognitionActive = false
stopIdleCheck()
BleService.closeCodec()
FileLogger.e(TAG, "语音识别错误: $error")
Log.e(TAG, "语音识别错误: $error")
sendEvent("error", mapOf(
"code" to "RECOGNITION_ERROR",
"message" to error
@ -382,7 +496,7 @@ object AgentService : CoroutineScope {
return true
} catch (e: Exception) {
isRecognitionActive = false
FileLogger.e(TAG, "启动语音识别失败: ${e.message}")
Log.e(TAG, "启动语音识别失败: ${e.message}")
sendEvent("error", mapOf(
"code" to "RECOGNITION_START_ERROR",
"message" to e.message.toString()
@ -405,9 +519,9 @@ object AgentService : CoroutineScope {
BleService.closeCodec()
isRecognitionActive = false
stopIdleCheck()
FileLogger.d(TAG, "语音识别已停止")
Log.d(TAG, "语音识别已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音识别异常: ${e.message}")
Log.e(TAG, "停止语音识别异常: ${e.message}")
isRecognitionActive = false
stopIdleCheck()
}
@ -447,9 +561,9 @@ object AgentService : CoroutineScope {
openAIService.cancelCurrentStream()
// 记录日志
FileLogger.d(TAG, "AI流输出已停止")
Log.d(TAG, "AI流输出已停止")
} catch (e: Exception) {
FileLogger.e(TAG, "停止AI流输出异常", e)
Log.e(TAG, "停止AI流输出异常", e)
// 确保状态被重置,即使发生异常
isAiStreaming = false
currentAiJob = null
@ -462,13 +576,13 @@ object AgentService : CoroutineScope {
*/
fun processTextInput(text: String, speakResponse: Boolean = false): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化")
Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false
}
if (text.isEmpty()) {
FileLogger.e(TAG, "文本输入不能为空")
Log.e(TAG, "文本输入不能为空")
sendEvent("error", mapOf("code" to "EMPTY_TEXT", "message" to "文本输入不能为空"))
return false
}
@ -495,7 +609,7 @@ object AgentService : CoroutineScope {
text: String,
speakResponse: Boolean = true
) {
FileLogger.d(TAG, "用户问题: $text")
Log.d(TAG, "用户问题: $text")
// 创建用户文本消息并处理
val userMessage = openAIService.createUserMessage(text)
@ -514,7 +628,7 @@ object AgentService : CoroutineScope {
text: String = "",
speakResponse: Boolean = false
) {
FileLogger.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
Log.d(TAG, "处理图片输入: ${if (text.isEmpty()) "无附加文本" else "附带文本: $text"}")
// 创建带图片的用户消息并处理
val userMessage = openAIService.createUserMessageWithImage(text, imageBase64)
@ -546,7 +660,6 @@ object AgentService : CoroutineScope {
// 设置状态为正在流式输出
isAiStreaming = true
// 使用历史记录作为上下文发送到OpenAI
val responseBuilder = StringBuilder()
var metadata:String = ""
@ -593,24 +706,23 @@ object AgentService : CoroutineScope {
addToHistoryMessages(userMessage)
}
openAIService.sendMessageStream(
messages = messagesWithSystemPrompt,
callback = object : OpenAIService.StreamCallback {
override fun onToken(token: String) {
responseBuilder.append(token)
if (speakResponse) {
azureTtsHelper?.speakStream(token)
ttsService?.speakStream(token)
}
// 发送流式回复token
sendEvent("assistant_token", mapOf("token" to token))
}
override fun onComplete() {
FileLogger.d(TAG, "AI完整回复: $responseBuilder")
Log.d(TAG, "AI完整回复: $responseBuilder")
// 视情况决定是否朗读回复
if (speakResponse) {
azureTtsHelper?.flushStream()
ttsService?.flushStream()
}
val response = responseBuilder.toString()
@ -638,7 +750,7 @@ object AgentService : CoroutineScope {
}
override fun onError(e: Exception) {
FileLogger.e(TAG, "AI处理出错", e)
Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to "AI_ERROR",
"message" to e.message.toString()
@ -661,7 +773,7 @@ object AgentService : CoroutineScope {
stopRecognition()
}
} catch (e: Exception) {
FileLogger.e(TAG, "处理函数调用事件失败", e)
Log.e(TAG, "处理函数调用事件失败", e)
}
}
@ -671,7 +783,7 @@ object AgentService : CoroutineScope {
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
))
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
Log.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoHandleFcunCallResult(functionCallResult);
}
}
@ -679,7 +791,7 @@ object AgentService : CoroutineScope {
} catch (e: Exception) {
val errorType = if (hasImage) "AI_IMAGE_PROCESS_ERROR" else "AI_PROCESS_ERROR"
FileLogger.e(TAG, "AI处理出错", e)
Log.e(TAG, "AI处理出错", e)
sendEvent("error", mapOf(
"code" to errorType,
"message" to e.message.toString()
@ -722,9 +834,9 @@ object AgentService : CoroutineScope {
}
}
FileLogger.d(TAG, "已加载${recentMessages.size}条历史记录")
Log.d(TAG, "已加载${recentMessages.size}条历史记录")
} catch (e: Exception) {
FileLogger.e(TAG, "加载聊天历史失败: ${e.message}")
Log.e(TAG, "加载聊天历史失败: ${e.message}")
}
}
@ -755,8 +867,8 @@ object AgentService : CoroutineScope {
isTtsSpeaking = true
restartIdleCheck() // 状态变化,重启检测
// 播放文本
azureTtsHelper?.speakText(text)
// 直接调用TTS,无需协程包装
ttsService?.speakOnce(text)
}
/**
@ -764,7 +876,8 @@ object AgentService : CoroutineScope {
*/
fun stopTts() {
if (isTtsSpeaking) {
azureTtsHelper?.stopSpeaking()
ttsService?.stop()
isTtsSpeaking = false
restartIdleCheck() // 状态变化,重启检测
sendEvent("tts_stopped", mapOf("status" to "stopped"))
@ -775,6 +888,7 @@ object AgentService : CoroutineScope {
* 保存聊天消息
*/
private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) {
// 显式指定IO调度器,数据库操作应在后台线程执行,即使父协程在Main上下文中
launch(Dispatchers.IO) {
try {
// 保存用户消息
@ -794,15 +908,15 @@ object AgentService : CoroutineScope {
)
// if (assistantMessageId != -1L) {
// FileLogger.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// Log.d(TAG, "聊天记录已保存:用户消息ID=$userMessageId, 助手消息ID=$assistantMessageId")
// } else {
// FileLogger.e(TAG, "保存助手消息失败")
// Log.e(TAG, "保存助手消息失败")
// }
} else {
FileLogger.e(TAG, "保存用户消息失败")
Log.e(TAG, "保存用户消息失败")
}
} catch (e: Exception) {
FileLogger.e(TAG, "保存聊天记录异常", e)
Log.e(TAG, "保存聊天记录异常", e)
}
}
}
@ -821,19 +935,17 @@ object AgentService : CoroutineScope {
historyMessages.remove(0)
}
}
FileLogger.d(TAG, "聊天历史已清除")
Log.d(TAG, "聊天历史已清除")
} else {
FileLogger.e(TAG, "清除聊天历史失败")
Log.e(TAG, "清除聊天历史失败")
}
withContext(Dispatchers.Main) {
callback(success)
}
// 由于launch已在Main上下文中,可以直接调用回调
callback(success)
} catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史异常: ${e.message}")
withContext(Dispatchers.Main) {
callback(false)
}
Log.e(TAG, "清除聊天历史异常: ${e.message}")
// 由于launch已在Main上下文中,可以直接调用回调
callback(false)
}
}
}
@ -842,57 +954,19 @@ object AgentService : CoroutineScope {
* 发送事件
*/
private fun sendEvent(eventName: String, data: Map<String, Any>) {
// 检查当前是否在主线程
if (Looper.myLooper() == Looper.getMainLooper()) {
// 已在主线程,直接发送
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
// 使用协程确保在主线程上执行
launch {
// 我们已在主线程上下文中启动协程,无需再切换线程
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
Log.e(TAG, "向监听器发送事件失败", e)
}
} else {
// 不在主线程,切换到主线程
Handler(Looper.getMainLooper()).post {
listeners.forEach { listener ->
try {
listener.onEvent(eventName, data)
} catch (e: Exception) {
FileLogger.e(TAG, "向监听器发送事件失败", e)
}
}
}
}
}
/**
* 释放所有资源
*/
fun dispose() {
try {
stopRecognition()
stopTts()
stopAiStream()
stopIdleCheck()
audioPlayer?.release()
audioPlayer = null
azureAsrHelper?.dispose()
azureTtsHelper?.dispose()
job.cancel()
clearListeners()
isInitialized = false
FileLogger.d(TAG, "代理服务资源已释放")
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源异常: ${e.message}")
}
}
/**
* 检查录音权限
*/
@ -907,13 +981,13 @@ object AgentService : CoroutineScope {
*/
fun processImageInput(imagePath: String, text: String = "", speakResponse: Boolean = false): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "服务未初始化")
Log.e(TAG, "服务未初始化")
sendEvent("error", mapOf("code" to "NOT_INITIALIZED", "message" to "服务未初始化"))
return false
}
if (imagePath.isEmpty()) {
FileLogger.e(TAG, "图片路径不能为空")
Log.e(TAG, "图片路径不能为空")
sendEvent("error", mapOf("code" to "EMPTY_IMAGE_PATH", "message" to "图片路径不能为空"))
return false
}
@ -945,7 +1019,7 @@ object AgentService : CoroutineScope {
// 处理包含图片的消息
processImageWithOpenAI(imageBase64, text, speakResponse)
} catch (e: Exception) {
FileLogger.e(TAG, "处理图片失败: ${e.message}")
Log.e(TAG, "处理图片失败: ${e.message}")
sendEvent("error", mapOf(
"code" to "IMAGE_PROCESSING_ERROR",
"message" to e.message.toString()
@ -993,9 +1067,9 @@ object AgentService : CoroutineScope {
}
}
}
FileLogger.d(TAG, "开始播放音频资源")
Log.d(TAG, "开始播放音频资源")
} catch (e: Exception) {
FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e)
Log.e(TAG, "播放音频资源异常: ${e.message}", e)
release()
}
}
@ -1017,17 +1091,17 @@ object AgentService : CoroutineScope {
}
}
}
/*
/*
* 自动播放音乐
* */
fun autoHandleFcunCallResult(functionCallResult: JSONObject): String{
val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr")
// Log.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic")
Log.d(TAG, "检查到音乐卡片: $cardMusic")
val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
@ -1049,13 +1123,13 @@ object AgentService : CoroutineScope {
playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
}
if (playlist.isNotEmpty()) {
FileLogger.w(TAG, "自动播放音乐列表 ${playlist}")
Log.w(TAG, "自动播放音乐列表 ${playlist}")
processMusicPlayList(playlist)
} else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目")
Log.w(TAG, "card_musiclist 中没有有效的音乐条目")
}
} else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组")
Log.w(TAG, "card_musiclist.musics 不是一个数组")
}
}else if (meta.has("card_navigation")) { //导航服务
val cardNavigation = meta.getJSONObject("card_navigation")
@ -1080,7 +1154,7 @@ object AgentService : CoroutineScope {
*/
fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
FileLogger.i(TAG, "启动音乐服务 播放音乐 $song")
Log.i(TAG, "启动音乐服务 播放音乐 $song")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
}
@ -1096,10 +1170,10 @@ object AgentService : CoroutineScope {
fun processMusicPlayList(songs:List<Map<String,String>>){
// 如果 playlist 中有有效的歌曲,开始播放
if (songs.isNotEmpty()) {
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
Log.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else {
FileLogger.i(TAG, "音乐列表为空,未启动播放服务")
Log.i(TAG, "音乐列表为空,未启动播放服务")
}
}
@ -1114,10 +1188,10 @@ object AgentService : CoroutineScope {
fun processNavigation(start:String,end:String){
// 如果 playlist 中有有效的歌曲,开始播放
if (!start.isNullOrEmpty() && !end.isNullOrEmpty()) {
FileLogger.i(TAG, "启动导航服务 ${start} ${end}")
NavigationServiceStarter.startServiceWithCommand(context, command = "start", startpos = start, endpos = end)
Log.i(TAG, "启动导航服务 ${start} ${end}")
// NavigationServiceStarter.startServiceWithCommand(context, command = "start", startpos = start, endpos = end)
} else {
FileLogger.i(TAG, "启动导航服务失败")
Log.i(TAG, "启动导航服务失败")
}
}
}

30
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt

@ -8,7 +8,7 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import org.json.JSONObject
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@ -33,7 +33,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
// 添加监听器
AgentService.addListener(this)
FileLogger.d(TAG, "插件已加载")
Log.d(TAG, "插件已加载")
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
@ -46,7 +46,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
return
}
FileLogger.d(TAG, "启动BleMediaService,配置参数: $config")
Log.d(TAG, "启动BleMediaService,配置参数: $config")
// 创建Intent启动BleMediaService
val intent = Intent(context, BleMediaService::class.java)
@ -59,6 +59,8 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) }
config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) }
config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) }
config["volcanoAppId"]?.toString()?.let { intent.putExtra("volcanoAppId", it) }
config["volcanoToken"]?.toString()?.let { intent.putExtra("volcanoToken", it) }
// 启动前台服务
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -69,7 +71,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "启动AgentService失败", e)
Log.e(TAG, "启动AgentService失败", e)
result.error("SERVICE_START_ERROR", "启动AgentService失败: ${e.message}", null)
}
}
@ -79,7 +81,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
context.stopService(intent)
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止AgentService失败", e)
Log.e(TAG, "停止AgentService失败", e)
result.error("SERVICE_STOP_ERROR", "停止AgentService失败: ${e.message}", null)
}
}
@ -88,7 +90,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
val isStarted = AgentService.startRecognition(false)
result.success(isStarted)
} catch (e: Exception) {
FileLogger.e(TAG, "启动对话失败", e)
Log.e(TAG, "启动对话失败", e)
result.error("START_ERROR", "启动对话失败: ${e.message}", null)
}
}
@ -97,7 +99,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.stopRecognition()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止对话失败", e)
Log.e(TAG, "停止对话失败", e)
result.error("STOP_ERROR", "停止对话失败: ${e.message}", null)
}
}
@ -118,7 +120,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(isProcessing)
} catch (e: Exception) {
FileLogger.e(TAG, "处理文本输入失败", e)
Log.e(TAG, "处理文本输入失败", e)
result.error("TEXT_PROCESS_ERROR", "处理文本输入失败: ${e.message}", null)
}
}
@ -141,7 +143,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(isProcessing)
} catch (e: Exception) {
FileLogger.e(TAG, "处理图片输入失败", e)
Log.e(TAG, "处理图片输入失败", e)
result.error("IMAGE_PROCESS_ERROR", "处理图片输入失败: ${e.message}", null)
}
}
@ -156,7 +158,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.speakText(text)
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "语音合成失败", e)
Log.e(TAG, "语音合成失败", e)
result.error("TTS_ERROR", "语音合成失败: ${e.message}", null)
}
}
@ -165,7 +167,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.stopTts()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败", e)
Log.e(TAG, "停止语音合成失败", e)
result.error("STOP_TTS_ERROR", "停止语音合成失败: ${e.message}", null)
}
}
@ -175,7 +177,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
result.success(success)
}
} catch (e: Exception) {
FileLogger.e(TAG, "清除聊天历史失败", e)
Log.e(TAG, "清除聊天历史失败", e)
result.error("CLEAR_HISTORY_ERROR", "清除聊天历史失败: ${e.message}", null)
}
}
@ -184,7 +186,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.interruptCurrentResponse()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "中断响应失败", e)
Log.e(TAG, "中断响应失败", e)
result.error("INTERRUPT_ERROR", "中断响应失败: ${e.message}", null)
}
}
@ -193,7 +195,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream
AgentService.dispose()
result.success(true)
} catch (e: Exception) {
FileLogger.e(TAG, "释放资源失败", e)
Log.e(TAG, "释放资源失败", e)
result.error("DISPOSE_ERROR", "释放资源失败: ${e.message}", null)
}
}

28
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt

@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service
import android.content.Context
import com.yunqiinnovation.agent_service.AgentService
import com.yunqiinnovation.agent_service.AgentServiceListener
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
@ -29,11 +29,11 @@ object BleAgent : BleService.Callback, AgentServiceListener {
if (isInitialized) return true
try {
FileLogger.d(TAG, "初始化语音代理..., $config")
Log.d(TAG, "初始化语音代理..., $config")
// 1. 初始化AgentService
if (!AgentService.initialize(context, config, true)) {
FileLogger.e(TAG, "语音服务初始化失败")
Log.e(TAG, "语音服务初始化失败")
return false
}
@ -41,20 +41,20 @@ object BleAgent : BleService.Callback, AgentServiceListener {
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
FileLogger.d(TAG, "已注册BLE回调")
Log.d(TAG, "已注册BLE回调")
} else {
FileLogger.w(TAG, "BLE回调注册失败")
Log.w(TAG, "BLE回调注册失败")
}
} else {
FileLogger.e(TAG, "BLE服务初始化失败")
Log.e(TAG, "BLE服务初始化失败")
return false
}
isInitialized = true
FileLogger.d(TAG, "语音代理初始化成功")
Log.d(TAG, "语音代理初始化成功")
return true
} catch (e: Exception) {
FileLogger.e(TAG, "初始化语音代理异常", e)
Log.e(TAG, "初始化语音代理异常", e)
return false
}
}
@ -64,7 +64,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
*/
fun startListening(): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "语音代理未初始化")
Log.e(TAG, "语音代理未初始化")
return false
}
@ -106,7 +106,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
AgentService.dispose()
isInitialized = false
FileLogger.d(TAG, "语音代理资源已释放")
Log.d(TAG, "语音代理资源已释放")
}
//============================================================================================
@ -119,16 +119,16 @@ object BleAgent : BleService.Callback, AgentServiceListener {
override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(TAG, "Ble设备已连接,准备好语音交互")
Log.d(TAG, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(TAG, "Ble设备已断开,停止语音服务")
Log.d(TAG, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务
// stopListening()
}
}
override fun onAudioDataReceived(data: ByteArray) {
// FileLogger.d(TAG, "onAudioDataReceived, data: ${data.size}")
// Log.d(TAG, "onAudioDataReceived, data: ${data.size}")
AgentService.pushAudioData(data)
// 可选:处理音频数据
}
@ -138,7 +138,7 @@ object BleAgent : BleService.Callback, AgentServiceListener {
* 在收到唤醒信号时启动语音识别
*/
override fun onWakeupSignalReceived() {
FileLogger.d(TAG, "收到唤醒信号,启动语音识别")
Log.d(TAG, "收到唤醒信号,启动语音识别")
// 停止当前TTS,避免冲突
AgentService.interruptCurrentResponse()

57
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt

@ -21,7 +21,7 @@ import android.support.v4.media.session.PlaybackStateCompat
import androidx.core.app.NotificationCompat
import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.agent_service.R
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
@ -56,6 +56,8 @@ class BleMediaService : Service(), CoroutineScope {
private const val KEY_OPENAI_MODEL = "openaiModel"
private const val KEY_SYSTEM_PROMPT = "systemPrompt"
private const val KEY_MCP_SERVER = "mcpServer"
private const val KEY_VOLCANO_APP_ID = "volcanoAppId"
private const val KEY_VOLCANO_TOKEN = "volcanoToken"
// 用于跟踪服务是否正在运行
private val isRunning = AtomicBoolean(false)
@ -89,8 +91,7 @@ class BleMediaService : Service(), CoroutineScope {
override fun onCreate() {
super.onCreate()
FileLogger.init(this)
FileLogger.i(TAG, "BleMediaService正在创建...")
Log.i(TAG, "BleMediaService正在创建...")
// 设置服务状态
isRunning.set(true)
@ -111,11 +112,11 @@ class BleMediaService : Service(), CoroutineScope {
audioPlayer?.play(R.raw.silence, 0f)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED)
FileLogger.i(TAG, "媒体服务已启动")
Log.i(TAG, "媒体服务已启动")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
Log.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}")
// 保存当前Intent,用于后续初始化
currentIntent = intent
@ -159,36 +160,36 @@ class BleMediaService : Service(), CoroutineScope {
// 如果Intent中有参数,则保存到SharedPreferences
if (configFromIntent.isNotEmpty()) {
FileLogger.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
Log.d(TAG, "从Intent中获取到配置参数,保存到SharedPreferences")
saveConfigToPreferences(configFromIntent, prefs)
}
// 合并Intent和SharedPreferences中的参数
val config = getConfigFromPreferences(prefs)
FileLogger.d(TAG, "当前配置参数: $config")
Log.d(TAG, "当前配置参数: $config")
// 检查必要的配置是否存在
if (config[KEY_AZURE_SPEECH_KEY].isNullOrEmpty() || config[KEY_OPENAI_API_KEY].isNullOrEmpty()) {
FileLogger.w(TAG, "语音代理初始化失败: API密钥未配置")
Log.w(TAG, "语音代理初始化失败: API密钥未配置")
return
}
if (isChatAgentInitialized) {
FileLogger.d(TAG, "ChatAgent已经初始化,跳过")
Log.d(TAG, "ChatAgent已经初始化,跳过")
return
}
if(config.isNotEmpty()) {
// 初始化ChatAgent
FileLogger.i(TAG, "开始初始化ChatAgent")
Log.i(TAG, "开始初始化ChatAgent")
val success = BleAgent.initialize(applicationContext, config)
if (success) {
isChatAgentInitialized = true
FileLogger.i(TAG, "语音交互代理初始化成功")
Log.i(TAG, "语音交互代理初始化成功")
} else {
FileLogger.e(TAG, "语音交互代理初始化失败")
Log.e(TAG, "语音交互代理初始化失败")
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "初始化语音交互代理时发生异常", e)
Log.e(TAG, "初始化语音交互代理时发生异常", e)
}
}
@ -206,8 +207,10 @@ class BleMediaService : Service(), CoroutineScope {
it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value }
it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value }
it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value }
it.getStringExtra(KEY_VOLCANO_APP_ID)?.let { value -> config[KEY_VOLCANO_APP_ID] = value }
it.getStringExtra(KEY_VOLCANO_TOKEN)?.let { value -> config[KEY_VOLCANO_TOKEN] = value }
}
FileLogger.i(TAG, "从Intent中提取到配置参数: $config")
Log.i(TAG, "从Intent中提取到配置参数: $config")
return config
}
@ -236,7 +239,9 @@ class BleMediaService : Service(), CoroutineScope {
KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"),
KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"),
KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"),
KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, "")
KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, ""),
KEY_VOLCANO_APP_ID to getConfigString(prefs, KEY_VOLCANO_APP_ID, ""),
KEY_VOLCANO_TOKEN to getConfigString(prefs, KEY_VOLCANO_TOKEN, "")
)
}
@ -250,15 +255,15 @@ class BleMediaService : Service(), CoroutineScope {
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
FileLogger.i(TAG, "BleMediaService正在销毁...")
Log.i(TAG, "BleMediaService正在销毁...")
// 取消注册媒体按钮接收器
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器已取消注册")
Log.i(TAG, "媒体按钮接收器已取消注册")
} catch (e: Exception) {
FileLogger.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
Log.e(TAG, "取消注册媒体按钮接收器失败: ${e.message}", e)
}
// 释放媒体会话
@ -319,16 +324,16 @@ class BleMediaService : Service(), CoroutineScope {
try {
val mediaButtonReceiver = ComponentName(packageName, MediaButtonReceiver::class.java.name)
audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver)
FileLogger.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
Log.i(TAG, "媒体按钮接收器注册成功: $mediaButtonReceiver")
} catch (e: Exception) {
FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
Log.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e)
}
}
private fun checkBleConnection() {
FileLogger.i(TAG, "checkBleConnection收到连接请求")
Log.i(TAG, "checkBleConnection收到连接请求")
try {
// 示例:使用协程异步处理连接
@ -338,7 +343,7 @@ class BleMediaService : Service(), CoroutineScope {
}
}
} catch (e: Exception) {
FileLogger.e(TAG, "连接设备失败: ${e.message}", e)
Log.e(TAG, "连接设备失败: ${e.message}", e)
}
}
@ -350,7 +355,7 @@ class BleMediaService : Service(), CoroutineScope {
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(NOTIFICATION_ID, createNotification())
} catch (e: Exception) {
FileLogger.e(TAG, "更新通知异常: ${e.message}")
Log.e(TAG, "更新通知异常: ${e.message}")
}
}
@ -451,7 +456,7 @@ class BleMediaService : Service(), CoroutineScope {
// // 播放完成后自动循环播放,保持媒体会话活跃
// setOnCompletionListener {
// FileLogger.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
// Log.i(TAG, "静音资源播放完成,再次播放保持会话活跃")
// play(resId, volume)
// }
@ -460,9 +465,9 @@ class BleMediaService : Service(), CoroutineScope {
start()
}
FileLogger.i(TAG, "开始播放静音资源")
Log.i(TAG, "开始播放静音资源")
} catch (e: Exception) {
FileLogger.e(TAG, "播放静音资源异常: ${e.message}", e)
Log.e(TAG, "播放静音资源异常: ${e.message}", e)
release()
}
}

14
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/utils.kt

@ -3,7 +3,7 @@ package com.yunqiinnovation.agent_service
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import com.yunqiinnovation.deepvoice_utils.FileLogger
import android.util.Log
/**
* 通用工具类,包含安全存储等功能
@ -59,10 +59,10 @@ object Utils {
// 提交更改
editor.apply()
FileLogger.d(TAG, "数据已安全保存到加密存储中")
Log.d(TAG, "数据已安全保存到加密存储中")
true
} catch (e: Exception) {
FileLogger.e(TAG, "保存数据到加密存储失败", e)
Log.e(TAG, "保存数据到加密存储失败", e)
false
}
}
@ -97,10 +97,10 @@ object Utils {
}
}
FileLogger.d(TAG, "已从加密存储加载所有数据")
Log.d(TAG, "已从加密存储加载所有数据")
resultMap
} catch (e: Exception) {
FileLogger.e(TAG, "从加密存储加载所有数据失败", e)
Log.e(TAG, "从加密存储加载所有数据失败", e)
emptyMap()
}
}
@ -134,10 +134,10 @@ object Utils {
.putString(KEY_SESSION_ID, sessionId)
.apply()
FileLogger.d(TAG, "已清除所有安全存储的数据")
Log.d(TAG, "已清除所有安全存储的数据")
true
} catch (e: Exception) {
FileLogger.e(TAG, "清除安全存储数据失败", e)
Log.e(TAG, "清除安全存储数据失败", e)
false
}
}

14
local_plugins/agent_service/pubspec.yaml

@ -10,20 +10,6 @@ environment:
dependencies:
flutter:
sdk: flutter
# 依赖本地插件
open_ai_service:
path: ../open_ai_service
azure_speech:
path: ../azure_speech
deepvoice_utils:
path: ../deepvoice_utils
chat_storage:
path: ../chat_storage
music_service:
path: ../music_service
amap_service:
path: ../amap_service
get: ^4.6.5
dev_dependencies:
flutter_test:

45
local_plugins/azure_speech/.gitignore

@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
local_plugins/azure_speech/.metadata

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

1
local_plugins/azure_speech/analysis_options.yaml

@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml

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

@ -52,6 +52,7 @@ tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
dependencies {
// 添加Microsoft语音SDK
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.30.0")
implementation(project(":ble_service"))
implementation("com.microsoft.cognitiveservices.speech:client-sdk:1.43.0")
implementation(project(":speech"))
}

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

@ -10,7 +10,7 @@ import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.media.audiofx.AutomaticGainControl
import android.os.Process
import com.yunqiinnovation.azure_speech.utils.FileLogger
import android.util.Log
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import com.microsoft.cognitiveservices.speech.util.EventHandler
@ -84,7 +84,7 @@ class AzureAsrHelper(private val context: Context) {
try {
// 检查配置是否为空
if (subscriptionKey.isEmpty() || region.isEmpty()) {
FileLogger.e(tag, "Azure 配置信息不完整")
Log.e(tag, "Azure 配置信息不完整")
return false
}
@ -124,7 +124,7 @@ class AzureAsrHelper(private val context: Context) {
// 创建识别器
return setupRecognizer()
} catch (e: Exception) {
FileLogger.e(tag, "初始化失败: ${e.message}")
Log.e(tag, "初始化失败: ${e.message}")
return false
}
}
@ -184,7 +184,7 @@ class AzureAsrHelper(private val context: Context) {
return true
} catch (e: Exception) {
FileLogger.e(tag, "创建识别器失败: ${e.message}")
Log.e(tag, "创建识别器失败: ${e.message}")
stopAudioProcessing()
return false
}
@ -201,8 +201,8 @@ class AzureAsrHelper(private val context: Context) {
// 创建音频配置 - 正确使用fromStreamInput方法,只传入回调
audioConfig = AudioConfig.fromStreamInput(microphoneStream)
} catch (e: Exception) {
FileLogger.e(tag, "设置麦克风流失败: ${e.message}")
e.printStackTrace()
Log.e(tag, "设置麦克风流失败: ${e.message}")
microphoneStream = null
}
}
@ -217,8 +217,8 @@ class AzureAsrHelper(private val context: Context) {
// 创建音频配置
audioConfig = AudioConfig.fromStreamInput(externalAudioStream)
} catch (e: Exception) {
FileLogger.e(tag, "设置外部音频流失败: ${e.message}")
e.printStackTrace()
Log.e(tag, "设置外部音频流失败: ${e.message}")
externalAudioStream = null
}
}
@ -368,23 +368,13 @@ class AzureAsrHelper(private val context: Context) {
// 取消事件
recognizer?.canceled?.addEventListener(
EventHandler<SpeechRecognitionCanceledEventArgs> { _, event ->
val errorDetails = try {
event.errorDetails ?: "未知错误"
} catch (e: Exception) {
"未知错误"
}
val errorDetails = event.errorDetails ?: "未知错误"
val reason = event.reason.toString()
FileLogger.e(tag, "识别取消: $errorDetails")
isContinuousRecognitionActive = false
stopAudioProcessing()
// 直接在当前线程调用回调
callback.onCanceled(reason, errorDetails)
callback.onError("识别取消: $errorDetails") // 兼容旧接口
}
)
}
/**
* 停止连续语音识别
@ -393,7 +383,7 @@ class AzureAsrHelper(private val context: Context) {
*/
fun stopContinuousRecognition(): Boolean {
if (speechConfig == null) {
FileLogger.e(tag, "语音服务未初始化")
Log.e(tag, "语音服务未初始化")
return false
}
@ -403,7 +393,7 @@ class AzureAsrHelper(private val context: Context) {
try {
if (recognizer == null) {
FileLogger.w(tag, "识别器为空,重置状态")
Log.w(tag, "识别器为空,重置状态")
isContinuousRecognitionActive = false
return true
}
@ -422,7 +412,7 @@ class AzureAsrHelper(private val context: Context) {
} catch (e: Exception) {
// 强制重置状态
isContinuousRecognitionActive = false
FileLogger.e(tag, "停止连续识别失败: ${e.message}")
Log.e(tag, "停止连续识别失败: ${e.message}")
// 停止音频处理
stopAudioProcessing()
@ -432,7 +422,7 @@ class AzureAsrHelper(private val context: Context) {
recognizer?.close()
recognizer = null
} catch (ex: Exception) {
FileLogger.e(tag, "关闭识别器失败: ${ex.message}")
Log.e(tag, "关闭识别器失败: ${ex.message}")
}
return false
@ -515,7 +505,7 @@ class AzureAsrHelper(private val context: Context) {
it.close()
microphoneStream = null
} catch (e: Exception) {
FileLogger.e(tag, "关闭麦克风流失败: ${e.message}")
Log.e(tag, "关闭麦克风流失败: ${e.message}")
e.printStackTrace()
}
}
@ -525,7 +515,7 @@ class AzureAsrHelper(private val context: Context) {
it.close()
externalAudioStream = null
} catch (e: Exception) {
FileLogger.e(tag, "关闭外部音频流失败: ${e.message}")
Log.e(tag, "关闭外部音频流失败: ${e.message}")
e.printStackTrace()
}
}
@ -623,12 +613,13 @@ class AzureAsrHelper(private val context: Context) {
try {
val result = audioRecord?.read(buffer, 0, buffer.size) ?: -1
if (result < 0) {
FileLogger.e(tag, "读取音频数据失败: $result")
Log.e(tag, "读取音频数据失败: $result")
return 0
}
return result
} catch (e: Exception) {
FileLogger.e(tag, "读取音频异常: ${e.message}")
return -1
Log.e(tag, "读取音频异常: ${e.message}")
return 0
}
}
@ -696,7 +687,7 @@ class AzureAsrHelper(private val context: Context) {
Thread.currentThread().interrupt()
return 0
} catch (e: Exception) {
FileLogger.e(tag, "读取外部音频数据失败: ${e.message}")
Log.e(tag, "读取外部音频数据失败: ${e.message}")
return 0
}
}

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

@ -11,21 +11,16 @@ import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.*
import kotlin.coroutines.CoroutineContext
import com.microsoft.cognitiveservices.speech.audio.*
import com.yunqiinnovation.ble_service.BleService
import com.yunqiinnovation.ble_service.BleConst
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
/** AzureSpeechPlugin */
class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
class AzureSpeechPlugin: FlutterPlugin {
private val tag = "AzureSpeechPlugin"
private lateinit var context: Context
private val mainHandler = Handler(Looper.getMainLooper())
// 协程相关
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// ASR相关
private lateinit var asrChannel : MethodChannel
@ -38,9 +33,8 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
private lateinit var ttsEventChannel: EventChannel
private var ttsEventSink: EventChannel.EventSink? = null
private lateinit var azureTtsHelper: AzureTtsHelper
// 是否已注册BLE回调
private var isRegisteredToBle = false
// 是否已添加TTS事件监听器
private var isTtsListenerAdded = false
// ASR 事件发送方法
private fun sendAsrEvent(event: Map<String, Any>) {
@ -66,7 +60,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
}
// 确保在主线程上发送事件
mainHandler.post {
try {
ttsEventSink?.success(event)
@ -104,6 +97,7 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
ttsEventChannel.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
ttsEventSink = events
setupTtsEventListener() // 在事件通道准备好时设置TTS事件监听器
}
override fun onCancel(arguments: Any?) {
@ -114,19 +108,53 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
// 初始化Azure语音服务
azureTtsHelper = AzureTtsHelper(context)
azureAsrHelper = AzureAsrHelper(context)
}
// 2. 初始化BleService并注册回调
if (BleService.initialize(context)) {
if (BleService.addCallback(this)) {
isRegisteredToBle = true
FileLogger.d(tag, "已注册BLE回调")
} else {
FileLogger.w(tag, "BLE回调注册失败")
}
} else {
FileLogger.e(tag, "BLE服务初始化失败")
/**
* 设置TTS事件监听器
*/
private fun setupTtsEventListener() {
if (!isTtsListenerAdded) {
// 添加TTS事件监听器
azureTtsHelper.addListener(object : TtsEventListener {
override fun onEvent(event: TtsEvent) {
// 将TTS事件转换为Flutter可理解的格式
val eventMap = when (event.type) {
TtsEventType.SYNTHESIS_STARTED -> mapOf(
"type" to "synthesis_started"
)
TtsEventType.SYNTHESIS_COMPLETED -> mapOf(
"type" to "synthesis_completed"
)
TtsEventType.SYNTHESIS_CANCELED -> {
val params = event.params
mapOf(
"type" to "synthesis_canceled",
"reason" to (params["reason"] ?: ""),
"errorDetails" to (params["errorDetails"] ?: "")
)
}
TtsEventType.ERROR -> {
val params = event.params
mapOf(
"type" to "error",
"errorCode" to (params["errorCode"] ?: "UNKNOWN_ERROR"),
"errorMessage" to (params["errorMessage"] ?: "未知错误")
)
}
else -> mapOf(
"type" to event.type.toString().lowercase(),
"params" to event.params
)
}
// 发送事件
sendTtsEvent(eventMap)
}
})
isTtsListenerAdded = true
}
}
// ASR方法处理器
@ -265,6 +293,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
azureAsrHelper.dispose()
result.success(true)
}
"pushAudioData" -> {
val data = call.argument<ByteArray>("data") ?: return result.error("INVALID_ARGUMENTS", "音频数据不能为空", null)
azureAsrHelper.pushAudioData(data)
result.success(true)
}
else -> {
result.notImplemented()
}
@ -279,34 +312,36 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
"initialize" -> {
val subscriptionKey = call.argument<String>("subscriptionKey") ?: ""
val region = call.argument<String>("region") ?: ""
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false // 默认使用自定义播放器
val useCustomAudioOutput = call.argument<Boolean>("useCustomAudioOutput") ?: false
// 先释放现有资源
azureTtsHelper.dispose()
// 创建TTS回调
val ttsCallback = createTtsCallback()
// 初始化TTS引擎
val success = azureTtsHelper.initialize(
subscriptionKey = subscriptionKey,
region = region,
callback = ttsCallback
)
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context)
try {
// 初始化TTS引擎(使用新的接口方法)
val success = azureTtsHelper.initialize(
ttsAppId = "", // Azure TTS不需要appId
ttsAppToken = subscriptionKey,
ttsResource = region,
language = "zh-CN"
)
// 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
// 如果初始化成功且需要使用自定义音频输出,则设置自定义音频播放器
if (success && useCustomAudioOutput) {
// 创建SimpleAudioPlayer实例
val customPlayer = SimpleAudioPlayer(context)
// 获取并设置音频输出流
val outputStream = customPlayer.getAudioOutputStream()
if (outputStream != null) {
azureTtsHelper.setCustomAudioOutputStream(outputStream)
}
}
// 确保设置了TTS事件监听器
setupTtsEventListener()
result.success(success)
} catch (e: Exception) {
result.error("INITIALIZATION_ERROR", "TTS初始化失败: ${e.message}", null)
}
result.success(success)
}
"setVoice" -> {
val voiceName = call.argument<String>("voiceName") ?: return result.error("INVALID_ARGUMENTS", "语音名称不能为空", null)
@ -315,18 +350,28 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
"speakText" -> {
val text = call.argument<String>("text") ?: return result.error("INVALID_ARGUMENTS", "文本不能为空", null)
// 直接传递参数给speakText方法
val success = azureTtsHelper.speakText(text)
result.success(success)
// 直接调用speakOnce方法
try {
val success = azureTtsHelper.speakOnce(text)
result.success(success)
} catch (e: Exception) {
result.error("SPEAK_ERROR", "语音合成失败: ${e.message}", null)
}
}
"stopSpeaking" -> {
result.success(azureTtsHelper.stopSpeaking())
// 直接调用stop方法
try {
val success = azureTtsHelper.stop()
result.success(success)
} catch (e: Exception) {
result.error("STOP_ERROR", "停止语音合成失败: ${e.message}", null)
}
}
"isSpeaking" -> {
result.success(azureTtsHelper.isSpeaking())
}
"dispose" -> {
azureTtsHelper.dispose()
azureTtsHelper.release()
result.success(true)
}
else -> {
@ -334,37 +379,6 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
}
}
}
/**
* 创建TTS回调
*/
private fun createTtsCallback(): AzureTtsHelper.TtsCallback {
return object : AzureTtsHelper.TtsCallback {
override fun onSynthesisStarted() {
sendTtsEvent(mapOf(
"type" to "synthesis_started"
))
}
override fun onSynthesizing() {
sendTtsEvent(mapOf(
"type" to "synthesizing"
))
}
override fun onSynthesisCompleted() {
sendTtsEvent(mapOf(
"type" to "synthesis_completed"
))
}
override fun onSynthesisCanceled() {
sendTtsEvent(mapOf(
"type" to "synthesis_canceled"
))
}
}
}
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
@ -374,49 +388,11 @@ class AzureSpeechPlugin: FlutterPlugin,BleService.Callback, CoroutineScope {
ttsEventChannel.setStreamHandler(null)
try {
// 取消所有协程
job.cancel()
// 取消BLE回调注册
if (isRegisteredToBle) {
BleService.removeCallback(this)
isRegisteredToBle = false
}
// 释放资源
azureTtsHelper.dispose()
azureTtsHelper.release()
azureAsrHelper.dispose()
} catch (e: Exception) {
FileLogger.e(tag, "释放资源失败: ${e.message}")
}
}
override fun onScanResult(devices: List<Map<String, Any>>) {
}
override fun onConnectionStateChanged(state: Int) {
// 可选:处理连接状态变化
if (state == BleConst.STATE_CONNECTED) {
FileLogger.d(tag, "Ble设备已连接,准备好语音交互")
} else if (state == BleConst.STATE_DISCONNECTED) {
FileLogger.d(tag, "Ble设备已断开,停止语音服务")
// 设备断开连接,停止语音服务
// stopListening()
}
}
override fun onAudioDataReceived(data: ByteArray) {
// FileLogger.d(tag, "AzureSpeechPlugin onAudioDataReceived, data: ${data.size}")
azureAsrHelper.pushAudioData(data)
}
override fun onWakeupSignalReceived() {
}
override fun onDeviceInfoReceived(infoType: Int, infoData: Map<String, Any>) {
}
}

300
local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt

@ -4,10 +4,14 @@ import android.content.Context
import com.microsoft.cognitiveservices.speech.*
import com.microsoft.cognitiveservices.speech.audio.*
import com.yunqiinnovation.azure_speech.utils.FileLogger
import com.deep_voice.speech.tts.ITtsService
import com.deep_voice.speech.tts.TtsEvent
import com.deep_voice.speech.tts.TtsEventListener
import com.deep_voice.speech.tts.TtsEventType
import com.deep_voice.speech.tts.AudioDataListener
import kotlinx.coroutines.*
import java.io.ByteArrayInputStream
import java.io.InputStream
import kotlin.coroutines.CoroutineContext
/**
* Azure TTS Helper
@ -15,18 +19,13 @@ import kotlin.coroutines.CoroutineContext
* 基于微软Azure语音服务的TTS实现
* 参考文档: https://learn.microsoft.com/en-us/azure/ai-services/speech-service/how-to-speech-synthesis
*/
class AzureTtsHelper(private val context: Context) : CoroutineScope {
class AzureTtsHelper(private val context: Context) : ITtsService {
companion object {
private const val TAG = "AzureTtsHelper"
private const val DEFAULT_LANGUAGE = "zh-CN"
private const val DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural"
}
// 协程上下文
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
// Azure语音服务配置
private var speechConfig: SpeechConfig? = null
private var synthesizer: SpeechSynthesizer? = null
@ -39,8 +38,9 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
private var currentPitch = "0%"
private var currentVolume = "100%"
// 全局回调
private var ttsCallback: TtsCallback? = null
// 事件监听器列表
private val eventListeners = mutableListOf<TtsEventListener>()
private val audioDataListeners = mutableListOf<AudioDataListener>()
// 自定义音频输出流
private var customAudioOutputStream: PushAudioOutputStream? = null
@ -48,54 +48,28 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 流式文本处理的缓冲区
private val streamBuffer = StringBuilder()
private var lastSpeakTime = 0L
/**
* 简化的TTS回调接口
*/
interface TtsCallback {
/**
* 合成开始
*/
fun onSynthesisStarted() {}
/**
* 合成中
*/
fun onSynthesizing() {}
/**
* 合成完成
*/
fun onSynthesisCompleted() {}
/**
* 合成取消
*/
fun onSynthesisCanceled() {}
}
/**
* 初始化TTS引擎
*
* @param subscriptionKey Azure语音服务订阅密钥
* @param region Azure语音服务区域
* @param callback 可选,全局TTS回调接口
* @param ttsAppId 未使用,保留为空即可
* @param ttsAppToken Azure语音服务订阅密钥
* @param ttsResource Azure语音服务区域
* @param language 语言代码,如"zh-CN"
* @return 是否初始化成功
*/
fun initialize(
subscriptionKey: String,
region: String,
callback: TtsCallback? = null
override fun initialize(
ttsAppId: String,
ttsAppToken: String,
ttsResource: String,
language: String
): Boolean {
try {
// 释放之前的资源
dispose()
// 存储全局回调
ttsCallback = callback
// 创建语音配置
speechConfig = SpeechConfig.fromSubscription(subscriptionKey, region)
speechConfig = SpeechConfig.fromSubscription(ttsAppToken, ttsResource)
speechConfig?.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
speechConfig?.setSpeechSynthesisVoiceName(currentVoice)
@ -115,7 +89,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
setupEventListeners()
isInitialized = true
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$region")
FileLogger.d(TAG, "TTS引擎初始化成功: 区域=$ttsResource")
return true
} catch (e: Exception) {
@ -148,12 +122,51 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
}
/**
* 设置TTS回调
*
* @param callback TTS回调接口
* 添加TTS事件监听器
*/
override fun addListener(listener: TtsEventListener) {
if (!eventListeners.contains(listener)) {
eventListeners.add(listener)
}
}
/**
* 移除TTS事件监听器
*/
override fun removeListener(listener: TtsEventListener) {
eventListeners.remove(listener)
}
/**
* 添加音频数据监听器
*/
override fun addAudioDataListener(listener: AudioDataListener) {
if (!audioDataListeners.contains(listener)) {
audioDataListeners.add(listener)
}
}
/**
* 移除音频数据监听器
*/
override fun removeAudioDataListener(listener: AudioDataListener) {
audioDataListeners.remove(listener)
}
/**
* 触发事件通知
*/
fun setTtsCallback(callback: TtsCallback?) {
ttsCallback = callback
private fun notifyEvent(eventType: TtsEventType, params: Map<String, Any> = emptyMap()) {
val event = TtsEvent(eventType, params)
// 复制列表防止并发修改异常
val listeners = ArrayList(eventListeners)
for (listener in listeners) {
try {
listener.onEvent(event)
} catch (e: Exception) {
FileLogger.e(TAG, "通知事件异常: ${e.message}")
}
}
}
/**
@ -163,33 +176,29 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
synthesizer?.apply {
// 合成开始事件
SynthesisStarted?.addEventListener { _, _ ->
ttsCallback?.onSynthesisStarted()
}
// 合成中事件
Synthesizing?.addEventListener { _, _ ->
ttsCallback?.onSynthesizing()
notifyEvent(TtsEventType.SYNTHESIS_STARTED)
}
// 合成完成事件
SynthesisCompleted?.addEventListener { _, _ ->
isSpeaking = false
ttsCallback?.onSynthesisCompleted()
notifyEvent(TtsEventType.SYNTHESIS_COMPLETED)
}
// 合成取消事件
SynthesisCanceled?.addEventListener { _, _ ->
SynthesisCanceled?.addEventListener { _, eventArgs ->
FileLogger.d(TAG, "语音合成取消")
isSpeaking = false
ttsCallback?.onSynthesisCanceled()
notifyEvent(TtsEventType.SYNTHESIS_CANCELED)
}
}
}
/**
* 设置语音
* 设置语音角色
*/
fun setVoice(voiceName: String): Boolean {
override fun setVoice(voiceName: String): Boolean {
if (!isInitialized) return false
try {
@ -199,6 +208,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "VOICE_SET_FAILED",
"errorMessage" to "设置语音失败: ${e.message}"
))
return false
}
}
@ -224,6 +237,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
setupEventListeners()
} catch (e: Exception) {
FileLogger.e(TAG, "重新创建合成器失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "RECREATE_FAILED",
"errorMessage" to "重新创建合成器失败: ${e.message}"
))
}
}
@ -240,6 +257,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
return true
} catch (e: Exception) {
FileLogger.e(TAG, "设置语音参数失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "PARAMS_SET_FAILED",
"errorMessage" to "设置语音参数失败: ${e.message}"
))
return false
}
}
@ -255,50 +276,52 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
* 生成SSML
*/
private fun generateSsml(rawText: String): String {
// 1. 定义要静音的符号和表情符号列表
val symbolsToMute = listOf(
"#", "*", "@", "%", "^", "&",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
)
// 1. 定义要静音的符号和表情符号列表
val symbolsToMute = listOf(
"#", "*", "@", "%", "^", "&",
"😀", "😂", "😊", "😍", "😢", "😎", "😉", "👍", "🙌", "🎉"
)
// 2. 转义 XML 保留字符
val escapedText = rawText
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
// 2. 转义 XML 保留字符
val escapedText = rawText
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
// 3. 静音处理特殊符号和表情符号
// 使用空白替换法,直接将符号替换为空格
var processedText = escapedText
symbolsToMute.forEach { sym ->
processedText = processedText.replace(sym, "")
}
// 3. 静音处理特殊符号和表情符号
// 使用空白替换法,直接将符号替换为空格
var processedText = escapedText
symbolsToMute.forEach { sym ->
processedText = processedText.replace(sym, "")
}
// 4. 构造简化的SSML文档,减少嵌套层级
return """
<speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN">
<voice name="$currentVoice">
<mstts:express-as style="cheerful">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
<say-as interpret-as="text">$processedText</say-as>
</prosody>
</mstts:express-as>
</voice>
</speak>
""".trimIndent()
}
// 4. 构造简化的SSML文档,减少嵌套层级
return """
<speak version="1.0"
xmlns="http://www.w3.org/2001/10/synthesis"
xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="zh-CN">
<voice name="$currentVoice">
<mstts:express-as style="cheerful">
<prosody rate="$currentRate" pitch="$currentPitch" volume="$currentVolume">
<say-as interpret-as="text">$processedText</say-as>
</prosody>
</mstts:express-as>
</voice>
</speak>
""".trimIndent()
}
/**
* 合成文本为语音
*
* @param text 要合成的文本
* @return 是否成功开始合成
* 单次播放文本(非流式)
*/
fun speakText(text: String): Boolean {
override fun speakOnce(text: String): Boolean {
if (!isInitialized) {
FileLogger.e(TAG, "TTS引擎未初始化")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
return false
}
@ -308,42 +331,51 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
isSpeaking = true
// 使用纯异步方式
// 直接调用SpeakSsmlAsync,SDK内部已经是异步的
synthesizer?.SpeakSsmlAsync(ssml)
// 返回true表示已开始合成请求
return true
} catch (e: Exception) {
isSpeaking = false
FileLogger.e(TAG, "语音合成异常: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "SYNTHESIS_ERROR",
"errorMessage" to "语音合成异常: ${e.message}"
))
return false
}
}
/**
* 处理流式文本并在遇到标点符号时播放
*
* @param text 收到的文本流片段
* @return 是否成功处理
* 流式合成文本
*/
fun speakStream(text: String): Boolean {
override fun speakStream(text: String): Boolean {
if (!isInitialized || text.isBlank()) {
if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
}
return false
}
try {
// 添加新文本到缓冲区
streamBuffer.append(text)
// 增加500ms防抖逻辑
val currentTime = System.currentTimeMillis()
if (currentTime - lastSpeakTime < 600) {
return true
}
lastSpeakTime = currentTime
val currentText = streamBuffer.toString()
// 定义标点符号列表
val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':')
val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n')
// 查找最后一个标点符号的位置
var lastPunctuationIndex = -1
@ -363,24 +395,30 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
streamBuffer.delete(0, lastPunctuationIndex + 1)
// 播放提取的文本
return speakText(textToSpeak)
return speakOnce(textToSpeak)
}
// 如果没有找到标点符号,则等待更多文本
return true
} catch (e: Exception) {
FileLogger.e(TAG, "流式语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STREAM_FAILED",
"errorMessage" to "流式语音合成失败: ${e.message}"
))
return false
}
}
/**
* 播放剩余的流式文本
*
* @return 是否成功播放剩余文本
*/
fun flushStream(): Boolean {
override fun flushStream(): Boolean {
if (!isInitialized) {
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "NOT_INITIALIZED",
"errorMessage" to "TTS引擎未初始化"
))
return false
}
@ -397,9 +435,13 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
}
// 播放剩余文本
return speakText(remainingText)
return speakOnce(remainingText)
} catch (e: Exception) {
FileLogger.e(TAG, "刷新流式文本失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "FLUSH_FAILED",
"errorMessage" to "刷新流式文本失败: ${e.message}"
))
return false
}
}
@ -407,7 +449,7 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/**
* 停止当前语音合成
*/
fun stopSpeaking(): Boolean {
override fun stop(): Boolean {
if (!isInitialized) return false
try {
@ -416,6 +458,11 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
isSpeaking = false
return true
} catch (e: Exception) {
FileLogger.e(TAG, "停止语音合成失败: ${e.message}")
notifyEvent(TtsEventType.ERROR, mapOf(
"errorCode" to "STOP_FAILED",
"errorMessage" to "停止语音合成失败: ${e.message}"
))
return false
}
}
@ -423,11 +470,25 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
/**
* 释放资源
*/
fun dispose() {
override fun release() {
dispose()
}
/**
* 释放资源(内部方法)
*/
private fun dispose() {
try {
stopSpeaking()
// 停止播放
stop()
// 关闭合成器
synthesizer?.close()
synthesizer = null
// 关闭配置
speechConfig?.close()
speechConfig = null
// 关闭自定义音频输出流
try {
@ -437,15 +498,10 @@ class AzureTtsHelper(private val context: Context) : CoroutineScope {
// 忽略关闭异常
}
// 清理引用
ttsCallback = null
// 清空流缓冲区
streamBuffer.clear()
// 取消所有协程
job.cancel()
// 重置状态
isInitialized = false
isSpeaking = false
} catch (e: Exception) {

34
local_plugins/azure_speech/ios/.gitignore

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

26
local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist

@ -0,0 +1,26 @@
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

1
local_plugins/azure_speech/ios/Flutter/Debug.xcconfig

@ -0,0 +1 @@
#include "Generated.xcconfig"

1
local_plugins/azure_speech/ios/Flutter/Release.xcconfig

@ -0,0 +1 @@
#include "Generated.xcconfig"

619
local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj

@ -0,0 +1,619 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
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 */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
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>"; };
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>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.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; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PNNU77KTCP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PNNU77KTCP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = PNNU77KTCP;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.azureSpeech;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

7
local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

6
ios/Runner/RunnerRelease.entitlements → local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -2,9 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.yunqiinnovation.deepsound</string>
</array>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

8
local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@ -0,0 +1,8 @@
<?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>PreviewsEnabled</key>
<false/>
</dict>
</plist>

99
local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

7
local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

8
local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

8
local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

@ -0,0 +1,8 @@
<?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>PreviewsEnabled</key>
<false/>
</dict>
</plist>

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

Loading…
Cancel
Save