diff --git a/.metadata b/.metadata index b4fb38531..5c982c8d4 100644 --- a/.metadata +++ b/.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 diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 48867bf1b..3dd7256fc 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -39,8 +39,6 @@ include(":spotify_sdk") include(":ble_service") include(":classic_bluetooth") include(":agent_service") -include(":volcano_speech") -include(":bytedance_speech") include(":speech") // 设置azure_speech项目的路径 project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android") @@ -51,6 +49,4 @@ project(":spotify_sdk").projectDir = file("../local_plugins/spotify_sdk/android" project(":ble_service").projectDir = file("../local_plugins/ble_service/android") project(":classic_bluetooth").projectDir = file("../local_plugins/classic_bluetooth/android") project(":agent_service").projectDir = file("../local_plugins/agent_service/android") -project(":volcano_speech").projectDir = file("../local_plugins/volcano_speech/android") -project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android") project(":speech").projectDir = file("../local_plugins/speech/android") \ No newline at end of file diff --git a/assets/config/GoogleService-Info.plist b/assets/config/GoogleService-Info.plist new file mode 100644 index 000000000..c90ada3a3 --- /dev/null +++ b/assets/config/GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 + ANDROID_CLIENT_ID + 907754492821-9jakqvathu61e6b8g95i41ssi7o4dnka.apps.googleusercontent.com + API_KEY + AIzaSyAvyBQcOXuO_q851vevqZIufsPRTMCe808 + GCM_SENDER_ID + 907754492821 + PLIST_VERSION + 1 + BUNDLE_ID + com.yunqiinnovation.deepsound + PROJECT_ID + deepsound-e0e3d + STORAGE_BUCKET + deepsound-e0e3d.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:907754492821:ios:246794fc44b23174145c9c + + \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index c10c27d8b..4c7418ab9 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,4 +1,4 @@ -# Uncomment this line to define a global platform for your project +# Define a global platform for your project platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. @@ -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 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cc790be8d..c5e3caec9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,8 +11,43 @@ PODS: - Flutter - audio_waveforms (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 +56,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 +124,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 +153,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 +177,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 +212,29 @@ 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`) + - 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 +246,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 +285,13 @@ SPEC REPOS: - GTMSessionFetcher - libwebp - Mantle - - MicrosoftCognitiveServicesSpeech-iOS + - QCloudCore + - QCloudCOSXML + - QCloudTrack - RecaptchaInterop - SDWebImage - SDWebImageWebPCoder + - SwiftyGif - Try - WechatOpenSDK-XCFramework @@ -222,8 +302,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/audio_session/ios" audio_waveforms: :path: ".symlinks/plugins/audio_waveforms/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 +332,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 +356,39 @@ 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 + 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 +399,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: 017ca79b199d8f37c4acb8d83ec912447003b920 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index b46ef17e1..82ce9f671 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/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,18 @@ /* 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 = ""; }; - 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 = ""; }; - 0A5EDBFB2D89C4FD00215E26 /* ClassicBluetoothHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicBluetoothHelper.swift; sourceTree = ""; }; - 0A5EDC032D8B544100215E26 /* BluetoothMediaButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothMediaButtonHelper.swift; sourceTree = ""; }; - 0A7A32E22D8DBA6D0047F746 /* VoiceInteractionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceInteractionService.swift; sourceTree = ""; }; - 0A7A32E32D8DBA6D0047F746 /* VolcanoAIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolcanoAIService.swift; sourceTree = ""; }; - 0ADE29672D8C25660032BFCB /* sample.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = sample.mp3; sourceTree = ""; }; - 0ADE29682D8C25660032BFCB /* silence.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = silence.mp3; sourceTree = ""; }; - 0ADE296B2D8C66D60032BFCB /* AzureAsrHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureAsrHelper.swift; sourceTree = ""; }; - 0ADE296C2D8C66D60032BFCB /* AzureTtsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AzureTtsHelper.swift; sourceTree = ""; }; - 0ADE296F2D8C6C450032BFCB /* AudioSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionManager.swift; sourceTree = ""; }; + 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 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 = ""; }; - 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 = ""; }; - 591293922D9524D1004A65BB /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 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 = ""; }; + 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 = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 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 = ""; }; + 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 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 = ""; }; - 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 = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -85,7 +62,12 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + 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 +75,49 @@ 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 = ""; }; - 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 */, + E27AD0DBE6531F2655DB9986 /* Pods_Runner.framework */, + F79ADB762804BBED65875C03 /* Pods_RunnerTests.framework */, ); - path = Pods; + name = Frameworks; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { @@ -147,8 +138,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, - 776E5AB1CE0A62CBDBAB3996 /* Pods */, - A5A9FDE94E78257E9A48E2E8 /* Frameworks */, + 1ACB686EC7DC36C6E33D372F /* Pods */, + 1E5DDD8701F6FB27AA502080 /* Frameworks */, ); sourceTree = ""; }; @@ -164,18 +155,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 +164,11 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */, ); path = Runner; sourceTree = ""; }; - A5A9FDE94E78257E9A48E2E8 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 4A9190D0838960F143BDC158 /* Pods_Runner.framework */, - 5BBE10AD7954527B6BB12CFB /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -203,10 +176,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 +195,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 +243,6 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; - packageReferences = ( - 5912938D2D95215F004A65BB /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, - ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -297,37 +267,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 +309,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 +326,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 +363,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 +400,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 +459,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 +494,15 @@ 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"; 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 +513,50 @@ }; 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; 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; 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; 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 +585,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 +643,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 +680,17 @@ 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"; 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 +704,15 @@ 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"; 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 +755,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 */; } diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 7ee26c6b2..000000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -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 -} diff --git a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 7ee26c6b2..000000000 --- a/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -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 -} diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 3bfd2c086..626664468 100644 --- a/ios/Runner/AppDelegate.swift +++ b/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) - } - } } diff --git a/ios/Runner/AudioSessionManager.swift b/ios/Runner/AudioSessionManager.swift deleted file mode 100644 index d41d2fbf5..000000000 --- a/ios/Runner/AudioSessionManager.swift +++ /dev/null @@ -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 - // } - // } - -} \ No newline at end of file diff --git a/ios/Runner/AzureAsrHelper.swift b/ios/Runner/AzureAsrHelper.swift deleted file mode 100644 index 5a2b73189..000000000 --- a/ios/Runner/AzureAsrHelper.swift +++ /dev/null @@ -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.size)), "启用输入端口") { - return false - } - - // 禁用输出端口 (我们只需要输入) - var enableOutput: UInt32 = 0 - if checkError(AudioUnitSetProperty(ioUnit!, kAudioOutputUnitProperty_EnableIO, - kAudioUnitScope_Output, kOutputBus, - &enableOutput, UInt32(MemoryLayout.size)), "禁用输出端口") { - return false - } - - // 设置缓冲区分配标志 - var flag: UInt32 = 0 - if checkError(AudioUnitSetProperty(ioUnit!, kAudioUnitProperty_ShouldAllocateBuffer, - kAudioUnitScope_Output, kInputBus, &flag, UInt32(MemoryLayout.size)), "设置缓冲区分配标志") { - return false - } - - // 设置音频格式 - let size = UInt32(MemoryLayout.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.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.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.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...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 - } -} \ No newline at end of file diff --git a/ios/Runner/AzureTtsHelper.swift b/ios/Runner/AzureTtsHelper.swift deleted file mode 100644 index f62c3f579..000000000 --- a/ios/Runner/AzureTtsHelper.swift +++ /dev/null @@ -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 """ - - - - \(text) - - - - """ - } - - /// 格式化语速参数 - 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 - } -} \ No newline at end of file diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index 19d3697a4..f3c28516f 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,10 +1,8 @@ - - - + + - - + @@ -16,14 +14,13 @@ - + - + - diff --git a/ios/Runner/BluetoothMediaButtonHelper.swift b/ios/Runner/BluetoothMediaButtonHelper.swift deleted file mode 100644 index 1a8369172..000000000 --- a/ios/Runner/BluetoothMediaButtonHelper.swift +++ /dev/null @@ -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.size) - } -} - -extension UInt16 { - var data: Data { - var int = self - return Data(bytes: &int, count: MemoryLayout.size) - } -} \ No newline at end of file diff --git a/ios/Runner/ClassicBluetoothHelper.swift b/ios/Runner/ClassicBluetoothHelper.swift deleted file mode 100644 index 2694075e0..000000000 --- a/ios/Runner/ClassicBluetoothHelper.swift +++ /dev/null @@ -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[.. -CFBundleURLTypes - - - CFBundleURLName - com.yunqiinnovation.deepsound - CFBundleURLSchemes - - fb3829022037361855 - com.googleusercontent.apps.907754492821-oc9e77svgmmlgouo0op7vv00e1hb8278 - - - -FacebookAppID -3829022037361855 -FacebookClientToken -193d0a2b1757c41cfa557ea8e8f21c33 -FacebookDisplayName -Deepsound - -LSApplicationQueriesSchemes - - fbapi - fb-messenger-api - fbauth2 - fbshareextension - googlechrome - googlechromes - - ITSAppUsesNonExemptEncryption - CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - DeepSound + Deep Voice CFBundleExecutable $(EXECUTABLE_NAME) - NSUserTrackingUsageDescription - This application needs to access your tracking data in order to offer better personalized services and advertisements CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion @@ -54,23 +22,57 @@ $(FLUTTER_BUILD_NAME) CFBundleSignature ???? + CFBundleURLTypes + + + CFBundleURLSchemes + + fb3829022037361855 + + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + wx5bbb7be9324f3e2d + + + CFBundleVersion $(FLUTTER_BUILD_NUMBER) + LSApplicationQueriesSchemes + + weixin + weixinULAPI + weixinURLParamsAPI + fbapi + fbapi20130214 + fbapi20130410 + fbapi20130702 + fbapi20131010 + fbapi20131219 + fbapi20140410 + fbapi20140116 + fbapi20150313 + fbapi20150629 + fbapi20160328 + fbauth + fb-messenger-share-api + fbauth2 + fbshareextension + LSRequiresIPhoneOS - NSBluetoothAlwaysUsageDescription - Bluetooth permission is required to connect and manage audio devices - NSBluetoothPeripheralUsageDescription - Bluetooth permission is required to connect and manage audio devices - NSMicrophoneUsageDescription - Microphone permission is required for voice recognition - NSSpeechRecognitionUsageDescription - Speech recognition permission is required to provide voice interaction services + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsInWebContent + + UIApplicationSupportsIndirectInputEvents - UIBackgroundModes - - UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -88,5 +90,23 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + NSBluetoothAlwaysUsageDescription + 需要蓝牙权限用于连接和管理音频设备 + NSBluetoothPeripheralUsageDescription + 需要蓝牙权限用于连接和管理音频设备 + NSLocationWhenInUseUsageDescription + 需要位置权限用于搜索附近的蓝牙设备 + NSLocationAlwaysAndWhenInUseUsageDescription + 需要位置权限用于搜索附近的蓝牙设备 + NSMicrophoneUsageDescription + 需要麦克风权限用于语音识别和录音功能 + FacebookClientToken + 193d0a2b1757c41cfa557ea8e8f21c33 + FacebookAppID + 3829022037361855 + FacebookAdvertiserIDCollectionEnabled + + FacebookAutoLogAppEventsEnabled + diff --git a/ios/Runner/Resources/.gitkeep b/ios/Runner/Resources/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/ios/Runner/Resources/sample.mp3 b/ios/Runner/Resources/sample.mp3 deleted file mode 100644 index 31ebc28ce..000000000 Binary files a/ios/Runner/Resources/sample.mp3 and /dev/null differ diff --git a/ios/Runner/Resources/silence.mp3 b/ios/Runner/Resources/silence.mp3 deleted file mode 100644 index f5bd92f88..000000000 Binary files a/ios/Runner/Resources/silence.mp3 and /dev/null differ diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h index 72042ccf3..308a2a560 100644 --- a/ios/Runner/Runner-Bridging-Header.h +++ b/ios/Runner/Runner-Bridging-Header.h @@ -1,7 +1 @@ -#ifndef Runner_Bridging_Header_h -#define Runner_Bridging_Header_h - #import "GeneratedPluginRegistrant.h" -#import - -#endif /* Runner_Bridging_Header_h */ diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 000000000..0c67376eb --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/ios/Runner/VoiceInteractionService.swift b/ios/Runner/VoiceInteractionService.swift deleted file mode 100644 index f19baf2c9..000000000 --- a/ios/Runner/VoiceInteractionService.swift +++ /dev/null @@ -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) - } - -} \ No newline at end of file diff --git a/ios/Runner/VolcanoAIService.swift b/ios/Runner/VolcanoAIService.swift deleted file mode 100644 index 4289b3b1b..000000000 --- a/ios/Runner/VolcanoAIService.swift +++ /dev/null @@ -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)" - } - } -} \ No newline at end of file diff --git a/ios/Runner/sample.mp3 b/ios/Runner/sample.mp3 deleted file mode 100644 index 31ebc28ce..000000000 Binary files a/ios/Runner/sample.mp3 and /dev/null differ diff --git a/ios/Runner/silence.mp3 b/ios/Runner/silence.mp3 deleted file mode 100644 index f5bd92f88..000000000 Binary files a/ios/Runner/silence.mp3 and /dev/null differ diff --git a/lib/data/services/login_service.dart b/lib/data/services/login_service.dart index d09ce9672..2844ef0f0 100644 --- a/lib/data/services/login_service.dart +++ b/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 _initializeFirebase() async { try { // 尝试获取已存在的实例 - Firebase.app('DeepSound'); + Firebase.app(); print('已存在的 Firebase 实例被重用'); } catch (e) { try { // 如果不存在则初始化新实例 await Firebase.initializeApp( - name: 'DeepSound', options: DefaultFirebaseOptions.currentPlatform, ); print('Firebase 初始化成功'); diff --git a/lib/data/services/speech_impl/volcano_tts_service.dart b/lib/data/services/speech_impl/volcano_tts_service.dart index 47d7aa2eb..402aac5b8 100644 --- a/lib/data/services/speech_impl/volcano_tts_service.dart +++ b/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; } diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 1f8b9593e..2fb1540d1 100644 --- a/lib/firebase_options.dart +++ b/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( diff --git a/lib/main.dart b/lib/main.dart index 72b596679..9086063c7 100644 --- a/lib/main.dart +++ b/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; diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 9ee8e9899..64668f38a 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/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 diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index d15f0da8e..653c849a2 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -9,6 +9,7 @@ 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 { @@ -83,6 +84,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; @@ -192,21 +198,66 @@ class PairingController extends GetxController { permissionsToRequest.add(Permission.bluetoothConnect); } } else if (Platform.isIOS) { - permissionsToRequest.add(Permission.bluetooth); + // iOS上单独处理蓝牙权限请求,避免冲突 + PermissionStatus status = await Permission.bluetooth.status; + + if (status.isPermanentlyDenied) { + Logger.warning('iOS蓝牙权限状态为permanentlyDenied,这可能是iOS系统误报'); + + // 在iOS上打开蓝牙设置对话框 + Get.dialog( + AlertDialog( + title: Text('需要蓝牙权限'), + content: Text('请在系统设置中手动开启蓝牙权限,以便连接设备'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('稍后'), + ), + TextButton( + onPressed: () { + Get.back(); + openAppSettings(); + }, + child: Text('前往设置'), + ), + ], + ), + barrierDismissible: false, + ); + return false; + } else if (!status.isGranted) { + // 单独请求蓝牙权限 + PermissionStatus result = await Permission.bluetooth.request(); + if (!result.isGranted) { + Logger.warning('iOS蓝牙权限申请失败: $result'); + return false; + } + } + + // iOS不再添加到批量请求列表 + // permissionsToRequest.add(Permission.bluetooth); } - - 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.warning('权限 ${permission.toString()} 未被授予: $status'); + } + }); + + if (!allGranted) { + return false; } - }); + } - _hasPermissions.value = allGranted; - return allGranted; + _hasPermissions.value = true; + return true; } // 辅助方法获取Android SDK版本 @@ -430,7 +481,15 @@ class PairingController extends GetxController { /// 打开应用设置 Future openAppSettings() async { - await AppSettings.openAppSettings(); + try { + await AppSettings.openAppSettings(); + // 返回后重新检查权限状态 + Future.delayed(Duration(milliseconds: 500), () { + reinitializeBluetooth(); + }); + } catch (e) { + Logger.error('打开应用设置失败: ${e.toString()}'); + } } /// 重新开始关联 @@ -447,4 +506,33 @@ class PairingController extends GetxController { _isNavigatingToHome = true; Get.offAllNamed(Routes.home); } + + /// 重新初始化蓝牙 + Future reinitializeBluetooth() async { + try { + isLoading.value = true; + statusDescription.value = '正在重新初始化蓝牙...'; + + // iOS上尝试重置权限状态 + if (Platform.isIOS) { + await Future.delayed(Duration(milliseconds: 500)); + if (await Permission.bluetooth.isPermanentlyDenied) { + Logger.info("iOS蓝牙权限仍处于永久拒绝状态,尝试直接使用蓝牙功能"); + // 在永久拒绝状态下,尝试使用蓝牙功能,iOS可能会再次弹出系统权限请求 + _bluetoothService.isBluetoothEnabled(); + } + } + + // 重新检查权限 + if (await _checkAndRequestPermissions()) { + // 重新检查蓝牙状态 + await _checkBluetoothAndRefresh(); + } + } catch (e) { + Logger.error('重新初始化蓝牙失败: ${e.toString()}'); + } finally { + isLoading.value = false; + _updateStatusDescription(); + } + } } \ No newline at end of file diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index cd7864e3c..e53f146dc 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -8,7 +8,6 @@ import '../../../data/models/user_Info.dart'; import '../../../routes/app_routes.dart'; import '../../../core/themes/theme_service.dart'; import '../../../data/services/network/api.dart'; -import 'package:amap_flutter/amap_flutter.dart'; class SplashController extends GetxController { final GetStorage _storage = GetStorage(); diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index b37d622a5..2a67d0ba0 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -48,7 +48,5 @@ dependencies { add("compileOnly", project(":ble_service")) add("compileOnly", project(":music_service")) add("compileOnly", project(":amap_service")) - add("compileOnly", project(":volcano_speech")) - add("compileOnly", project(":bytedance_speech")) add("compileOnly", project(":speech")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 5c40eaaeb..bda9b148b 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -21,8 +21,7 @@ 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.deep_voice.speech.tts.ITtsService -import com.deep_voice.bytedance_speech.BytedanceTTS +import com.yunqiinnovation.azure_speech.AzureTtsHelper /** * 代理服务事件监听接口 */ @@ -55,8 +54,8 @@ object AgentService : CoroutineScope { // Azure服务 private var azureAsrHelper: AzureAsrHelper? = null - // 使用接口类型,支持不同实现 - private var ttsService: ITtsService? = null + // 使用AzureTtsHelper作为唯一的TTS实现 + private var ttsService: AzureTtsHelper? = null // OpenAI服务 private lateinit var openAIService: OpenAIService @@ -247,65 +246,37 @@ object AgentService : CoroutineScope { } /** - * 初始化TTS引擎 + * 初始化TTS引擎,只使用火山语音引擎 */ private fun initializeTtsEngine(config: Map) { // 停止当前TTS服务(如果有) if (ttsService != null) { ttsService?.stop() + ttsService = null } try { - val ttsEngine = config["ttsEngine"]?.toString() ?: "volcano" // 默认使用火山引擎 + // 从配置中获取TTS参数 + val ttsAppId = config["volcanoAppId"]?.toString() ?: "" + val ttsAppToken = config["volcanoToken"]?.toString() ?: "" val ttsLanguage = config["ttsLanguage"]?.toString() ?: "zh-CN" - - // 根据配置选择合适的TTS引擎 - if (ttsEngine == "azure") { - // 使用Azure TTS - val azureSpeechKey = config["azureSpeechKey"]?.toString() ?: "" - val azureSpeechRegion = config["azureSpeechRegion"]?.toString() ?: "" - - // 创建并初始化Azure TTS Helper - val azureTtsHelper = com.yunqiinnovation.azure_speech.AzureTtsHelper(context) - ttsService = azureTtsHelper - - // 直接初始化,无需协程包装 - val success = azureTtsHelper.initialize( - ttsAppId = "", // Azure TTS不需要appId - ttsAppToken = azureSpeechKey, - ttsResource = azureSpeechRegion, - language = ttsLanguage - ) - - if (success) { - Log.d(TAG, "Azure TTS服务初始化成功") - } else { - Log.e(TAG, "Azure TTS服务初始化失败") - } - } else { - // 默认使用火山引擎TTS - // 创建并初始化BytedanceTTS - - // 从配置中获取TTS参数 - val ttsAppId = config["volcanoAppId"]?.toString() ?: "" - val ttsAppToken = config["volcanoToken"]?.toString() ?: "" - val bytedanceTts = BytedanceTTS(context) - ttsService = bytedanceTts - - // 初始化火山TTS - val success = bytedanceTts.initialize( - ttsAppId = ttsAppId, - ttsAppToken = ttsAppToken, - ttsResource = "", // 火山TTS不需要resource - language = ttsLanguage - ) - - if (success) { - Log.d(TAG, "火山TTS服务初始化请求已发送") - } else { - Log.e(TAG, "火山TTS服务初始化失败") - } + // 创建并初始化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事件监听 @@ -742,7 +713,6 @@ object AgentService : CoroutineScope { override fun onToken(token: String) { responseBuilder.append(token) if (speakResponse) { - // 直接调用TTS,无需协程包装 ttsService?.speakStream(token) } // 发送流式回复token @@ -753,7 +723,6 @@ object AgentService : CoroutineScope { Log.d(TAG, "AI完整回复: $responseBuilder") // 视情况决定是否朗读回复 if (speakResponse) { - // 直接调用TTS,无需协程包装 ttsService?.flushStream() } val response = responseBuilder.toString() @@ -1130,7 +1099,7 @@ object AgentService : CoroutineScope { val metaStr = functionCallResult.optString("meta") if (metaStr.isNotEmpty()) { val meta = JSONObject(metaStr) - Log.d(TAG, "mcp调用结果: $metaStr") + // Log.d(TAG, "mcp调用结果: $metaStr") if (meta.has("card_music")) { //音乐卡片 val cardMusic = meta.getJSONObject("card_music") Log.d(TAG, "检查到音乐卡片: $cardMusic") diff --git a/local_plugins/azure_speech/.gitignore b/local_plugins/azure_speech/.gitignore new file mode 100644 index 000000000..79c113f9b --- /dev/null +++ b/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 diff --git a/local_plugins/azure_speech/.metadata b/local_plugins/azure_speech/.metadata new file mode 100644 index 000000000..5c982c8d4 --- /dev/null +++ b/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' diff --git a/local_plugins/azure_speech/analysis_options.yaml b/local_plugins/azure_speech/analysis_options.yaml new file mode 100644 index 000000000..f9b303465 --- /dev/null +++ b/local_plugins/azure_speech/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt index 5a27836c7..70227503f 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureTtsHelper.kt @@ -375,7 +375,7 @@ class AzureTtsHelper(private val context: Context) : ITtsService { val currentText = streamBuffer.toString() // 定义标点符号列表 - val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':') + val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':', '\n') // 查找最后一个标点符号的位置 var lastPunctuationIndex = -1 diff --git a/local_plugins/azure_speech/ios/.gitignore b/local_plugins/azure_speech/ios/.gitignore new file mode 100644 index 000000000..7a7f9873a --- /dev/null +++ b/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 diff --git a/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist b/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000..7c5696400 --- /dev/null +++ b/local_plugins/azure_speech/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig b/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000..592ceee85 --- /dev/null +++ b/local_plugins/azure_speech/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/local_plugins/azure_speech/ios/Flutter/Release.xcconfig b/local_plugins/azure_speech/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000..592ceee85 --- /dev/null +++ b/local_plugins/azure_speech/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..8b82c852b --- /dev/null +++ b/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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* 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 = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; +/* 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner/RunnerRelease.entitlements b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 60% rename from ios/Runner/RunnerRelease.entitlements rename to local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 62a4a38c3..18d981003 100644 --- a/ios/Runner/RunnerRelease.entitlements +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -2,9 +2,7 @@ - keychain-access-groups - - $(AppIdentifierPrefix)com.yunqiinnovation.deepsound - + IDEDidComputeMac32BitWarning + diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..15cada483 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata b/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..1d526a16e --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/local_plugins/azure_speech/ios/Runner/AppDelegate.swift b/local_plugins/azure_speech/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000..626664468 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d36b1fab2 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000..dc9ada472 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..7353c41ec Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..6ed2d933e Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..4cd7b0099 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..fe730945a Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..321773cd8 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000..502f463a9 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000..e9f5fea27 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..84ac32ae7 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..8953cba09 Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..0467bf12a Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000..0bedcf2fd --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000..89c2725b7 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard b/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..f2e259c7c --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard b/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000..f3c28516f --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/local_plugins/azure_speech/ios/Runner/Info.plist b/local_plugins/azure_speech/ios/Runner/Info.plist new file mode 100644 index 000000000..1d54d7cd7 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Azure Speech + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + azure_speech + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h b/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000..308a2a560 --- /dev/null +++ b/local_plugins/azure_speech/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift b/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..86a7c3b1b --- /dev/null +++ b/local_plugins/azure_speech/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/local_plugins/azure_speech/ios/azure_speech.podspec b/local_plugins/azure_speech/ios/azure_speech.podspec index bc179f0c1..b6eed7b01 100644 --- a/local_plugins/azure_speech/ios/azure_speech.podspec +++ b/local_plugins/azure_speech/ios/azure_speech.podspec @@ -16,7 +16,7 @@ A Flutter plugin for Azure Speech services. s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.dependency 'MicrosoftCognitiveServicesSpeech-iOS' - s.platform = :ios, '12.0' + s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/local_plugins/azure_speech/lib/main.dart b/local_plugins/azure_speech/lib/main.dart new file mode 100644 index 000000000..a7256585a --- /dev/null +++ b/local_plugins/azure_speech/lib/main.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: Text('Hello World!'), + ), + ), + ); + } +} diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 8d53a0950..999d56c3a 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -101,7 +101,7 @@ object BleService { private val mainHandler = Handler(Looper.getMainLooper()) // ota解码器实例 - var otaManager: OTAManager? = null + // var otaManager: OTAManager? = null // 初始化状态 private var isInitialized = false @@ -117,19 +117,19 @@ object BleService { try { this.context = appContext.applicationContext - otaManager = OTAManager(this.context).apply { - // 设置数据回调 - setDataCallback(object : OTAManager.DataCallback { - override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) { - data?.let { - Log.i("BleService", "收到数据:${it.size} 字节") - Log.i("BleService", "认证交互数据${it.contentToString()}") - // 移除次数限制,除非明确需要 - otaManager?.onReceiveDeviceData(device, it) - } - } - }) - } + // otaManager = OTAManager(this.context).apply { + // // 设置数据回调 + // setDataCallback(object : OTAManager.DataCallback { + // override fun onDataReceived(device: BluetoothDevice?, data: ByteArray?) { + // data?.let { + // Log.i("BleService", "收到数据:${it.size} 字节") + // Log.i("BleService", "认证交互数据${it.contentToString()}") + // // 移除次数限制,除非明确需要 + // otaManager?.onReceiveDeviceData(device, it) + // } + // } + // }) + // } // 初始化蓝牙管理器和适配器 bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager @@ -428,44 +428,44 @@ object BleService { } } - fun startOTA() { - try { - otaManager?.startOTA(CustomUpdateCallback(context, null)) - Log.i(TAG, "OTA 升级已启动") - } catch (e: IllegalStateException) { - Log.e(TAG, "OTA 启动失败: ${e.message}") - disconnectGatt() - } - } - - // 配置OTA - fun configureOTA() { - try { - val firmwarePath = - context.getExternalFilesDir("downloads/ota_upgrades")?.path + "/1.ufw" - val file = File(firmwarePath) - if (!file.exists()) { - // throw IOException("Firmware file not found") - Log.i(TAG, "Firmware file not found") - } - - val bluetoothOption = BluetoothOTAConfigure().apply { - priority = BluetoothOTAConfigure.PREFER_BLE - isUseReconnect = true - isUseAuthDevice = true - mtu = 512 - isNeedChangeMtu = false // 确保MTU请求生效 - isUseJLServer = false - firmwareFilePath = firmwarePath - } - otaManager?.configure(bluetoothOption) ?: run { - Log.e(TAG, "OTA Manager未初始化") - } - } catch (e: Exception) { - Log.e(TAG, "OTA配置失败: ${e.message}") - // 处理错误 - } - } + // fun startOTA() { + // try { + // otaManager?.startOTA(CustomUpdateCallback(context, null)) + // Log.i(TAG, "OTA 升级已启动") + // } catch (e: IllegalStateException) { + // Log.e(TAG, "OTA 启动失败: ${e.message}") + // disconnectGatt() + // } + // } + + // // 配置OTA + // fun configureOTA() { + // try { + // val firmwarePath = + // context.getExternalFilesDir("downloads/ota_upgrades")?.path + "/1.ufw" + // val file = File(firmwarePath) + // if (!file.exists()) { + // // throw IOException("Firmware file not found") + // Log.i(TAG, "Firmware file not found") + // } + + // val bluetoothOption = BluetoothOTAConfigure().apply { + // priority = BluetoothOTAConfigure.PREFER_BLE + // isUseReconnect = true + // isUseAuthDevice = true + // mtu = 512 + // isNeedChangeMtu = false // 确保MTU请求生效 + // isUseJLServer = false + // firmwareFilePath = firmwarePath + // } + // otaManager?.configure(bluetoothOption) ?: run { + // Log.e(TAG, "OTA Manager未初始化") + // } + // } catch (e: Exception) { + // Log.e(TAG, "OTA配置失败: ${e.message}") + // // 处理错误 + // } + // } /** * 更新连接状态并通知回调 @@ -481,35 +481,35 @@ object BleService { private val gattCallback = object : BluetoothGattCallback() { - override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { - if (status == BluetoothGatt.GATT_SUCCESS) { - Log.i(TAG, "MTU 更新成功: $mtu") - configureOTA() + // override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { + // if (status == BluetoothGatt.GATT_SUCCESS) { + // Log.i(TAG, "MTU 更新成功: $mtu") + // configureOTA() - startOTA() - } else { - Log.e(TAG, "MTU 更新失败: status=$status") - } - } + // startOTA() + // } else { + // Log.e(TAG, "MTU 更新失败: status=$status") + // } + // } override fun onConnectionStateChange(g: BluetoothGatt, status: Int, newState: Int) { when { status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED -> { updateConnectionState(BleConst.STATE_CONNECTED) g.discoverServices() - Log.i(TAG, "连接成功,开始 MTU 协商") + // Log.i(TAG, "连接成功,开始 MTU 协商") - otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK) - g.requestMtu(512) // 触发 MTU 修改流程 + // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_OK) + // g.requestMtu(512) // 触发 MTU 修改流程 } newState == BluetoothProfile.STATE_DISCONNECTED -> { updateConnectionState(BleConst.STATE_DISCONNECTED) disconnectGatt() - otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING) - g.close() - otaManager?.release(); + // otaManager?.onBtDeviceConnection(g.device, StateCode.CONNECTION_CONNECTING) + // g.close() + // otaManager?.release(); } else -> { diff --git a/local_plugins/chat_storage/ios/Classes/.gitkeep b/local_plugins/chat_storage/ios/Classes/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift b/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift index d0702e65c..e008b2e78 100644 --- a/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift +++ b/local_plugins/chat_storage/ios/Classes/ChatStorageHelper.swift @@ -1,410 +1,237 @@ import Foundation import SQLite3 -/// ChatStorageHelper - iOS版本的聊天存储助手 -/// 负责SQLite数据库操作和业务逻辑 +/** + * ChatStorageHelper - 聊天数据存储助手 + * 负责处理SQLite数据库操作,提供消息存储和检索功能 + */ class ChatStorageHelper { - // MARK: - Constants - private let TAG = "ChatStorageHelper" - - // 数据库常量 - private let DATABASE_NAME = "chat_messages.db" - private let DATABASE_VERSION = 1 - - // 表和列名 - private let TABLE_MESSAGES = "messages" - private let COLUMN_ID = "id" - private let COLUMN_SESSION_ID = "session_id" - private let COLUMN_MESSAGE = "message" - private let COLUMN_SENDER = "sender" - private let COLUMN_TIMESTAMP = "timestamp" - private let COLUMN_METADATA = "metadata" - - // MARK: - Properties private var db: OpaquePointer? private let dbPath: String - // MARK: - Initialization - init(_ context: Any) { - // 获取数据库文件路径 - let fileManager = FileManager.default - if let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first { - let databaseDirectory = documentsDirectory.appendingPathComponent("Databases") - - // 确保目录存在 - try? fileManager.createDirectory(at: databaseDirectory, withIntermediateDirectories: true) - - dbPath = databaseDirectory.appendingPathComponent(DATABASE_NAME).path - - // 打开数据库连接 - openDatabase() - } else { - dbPath = "" - print("\(TAG) 无法获取文档目录路径") - } - } - - deinit { - closeDatabase() - } - - // MARK: - Database Operations - - /// 打开数据库连接 - private func openDatabase() { - if sqlite3_open(dbPath, &db) == SQLITE_OK { - createTables() - print("\(TAG) 数据库已成功打开") - } else { - print("\(TAG) 无法打开数据库: \(String(describing: errorMessage))") - } - } - - /// 关闭数据库连接 - private func closeDatabase() { - if db != nil { - sqlite3_close(db) - db = nil + init() { + // 获取文档目录路径 + let fileURL = try! FileManager.default + .url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + .appendingPathComponent("chat_storage.db") + + dbPath = fileURL.path + + // 打开数据库 + if sqlite3_open(dbPath, &db) != SQLITE_OK { + print("无法打开数据库") + return } - } - - /// 创建必要的数据库表 - private func createTables() { + // 创建消息表 - let createTableSQL = """ - CREATE TABLE IF NOT EXISTS \(TABLE_MESSAGES) ( - \(COLUMN_ID) INTEGER PRIMARY KEY AUTOINCREMENT, - \(COLUMN_SESSION_ID) TEXT NOT NULL, - \(COLUMN_MESSAGE) TEXT NOT NULL, - \(COLUMN_SENDER) TEXT NOT NULL, - \(COLUMN_TIMESTAMP) TEXT NOT NULL, - \(COLUMN_METADATA) TEXT - ); + let createTableString = """ + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + message TEXT NOT NULL, + sender TEXT NOT NULL, + metadata TEXT, + created_at INTEGER NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_session_id ON messages (session_id); + CREATE INDEX IF NOT EXISTS idx_created_at ON messages (created_at); """ - if executeUpdate(sql: createTableSQL) { - // 创建索引以加快查询速度 - executeUpdate(sql: "CREATE INDEX IF NOT EXISTS idx_session_id ON \(TABLE_MESSAGES) (\(COLUMN_SESSION_ID))") - executeUpdate(sql: "CREATE INDEX IF NOT EXISTS idx_timestamp ON \(TABLE_MESSAGES) (\(COLUMN_TIMESTAMP))") - } - } - - /// 获取SQLite错误消息 - private var errorMessage: String { - if let errorPointer = sqlite3_errmsg(db) { - let errorMessage = String(cString: errorPointer) - return errorMessage - } else { - return "未知错误" + if sqlite3_exec(db, createTableString, nil, nil, nil) != SQLITE_OK { + let errmsg = String(cString: sqlite3_errmsg(db)!) + print("创建表失败: \(errmsg)") } } - /// 执行SQL更新操作(INSERT, UPDATE, DELETE, CREATE等) - private func executeUpdate(sql: String, parameters: [Any]? = nil) -> Bool { - var statement: OpaquePointer? - - if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { - // 绑定参数 - if let params = parameters { - for (index, param) in params.enumerated() { - bindParameter(statement: statement, index: index + 1, value: param) - } - } - - // 执行语句 - let result = sqlite3_step(statement) - sqlite3_finalize(statement) - - return result == SQLITE_DONE - } else { - print("\(TAG) 准备SQL语句失败: \(errorMessage)") - return false - } - } - - /// 绑定SQL参数 - private func bindParameter(statement: OpaquePointer?, index: Int, value: Any) { - if let value = value as? Int { - sqlite3_bind_int(statement, Int32(index), Int32(value)) - } else if let value = value as? Double { - sqlite3_bind_double(statement, Int32(index), value) - } else if let value = value as? String { - sqlite3_bind_text(statement, Int32(index), (value as NSString).utf8String, -1, nil) - } else if value is NSNull { - sqlite3_bind_null(statement, Int32(index)) - } - } - - /// 查询数据并处理结果 - private func executeQuery(sql: String, parameters: [Any]? = nil, handler: (OpaquePointer) -> Void) { - var statement: OpaquePointer? - - if sqlite3_prepare_v2(db, sql, -1, &statement, nil) == SQLITE_OK { - // 绑定参数 - if let params = parameters { - for (index, param) in params.enumerated() { - bindParameter(statement: statement, index: index + 1, value: param) - } - } - - // 处理结果 - while sqlite3_step(statement) == SQLITE_ROW { - handler(statement!) - } - } else { - print("\(TAG) 执行查询失败: \(errorMessage)") + deinit { + if db != nil { + sqlite3_close(db) } - - sqlite3_finalize(statement) } - // MARK: - Public Methods - - /// 保存消息 - /// - Parameters: - /// - sessionId: 会话ID - /// - message: 消息内容 - /// - sender: 发送者 - /// - metadata: 元数据 - /// - Returns: 成功返回消息ID,失败返回-1 - func saveMessage(sessionId: String, message: String, sender: String, metadata: String? = nil) -> Int64 { - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - let timestamp = dateFormatter.string(from: Date()) - - let sql = """ - INSERT INTO \(TABLE_MESSAGES) (\(COLUMN_SESSION_ID), \(COLUMN_MESSAGE), \(COLUMN_SENDER), \(COLUMN_TIMESTAMP), \(COLUMN_METADATA)) - VALUES (?, ?, ?, ?, ?) - """ - - var parameters: [Any] = [sessionId, message, sender, timestamp] - if let metadata = metadata { - parameters.append(metadata) + /** + * 保存消息到数据库 + * @param sessionId 会话ID + * @param message 消息内容 + * @param sender 发送者("user" 或 "agent") + * @param metadata 可选的元数据 + * @return 插入的消息ID,失败则返回-1 + */ + func saveMessage(sessionId: String, message: String, sender: String, metadata: String?) -> Int64 { + let insertStatementString = "INSERT INTO messages (session_id, message, sender, metadata, created_at) VALUES (?, ?, ?, ?, ?);" + var insertStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, insertStatementString, -1, &insertStatement, nil) == SQLITE_OK { + sqlite3_bind_text(insertStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 2, (message as NSString).utf8String, -1, nil) + sqlite3_bind_text(insertStatement, 3, (sender as NSString).utf8String, -1, nil) + + if let metadata = metadata { + sqlite3_bind_text(insertStatement, 4, (metadata as NSString).utf8String, -1, nil) + } else { + sqlite3_bind_null(insertStatement, 4) + } + + let currentTime = Int(Date().timeIntervalSince1970) + sqlite3_bind_int(insertStatement, 5, Int32(currentTime)) + + if sqlite3_step(insertStatement) == SQLITE_DONE { + let id = sqlite3_last_insert_rowid(db) + sqlite3_finalize(insertStatement) + return id + } else { + print("插入消息失败: \(String(cString: sqlite3_errmsg(db)!))") + } + + sqlite3_finalize(insertStatement) } else { - parameters.append(NSNull()) + print("插入消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") } - if executeUpdate(sql: sql, parameters: parameters) { - return sqlite3_last_insert_rowid(db) - } else { - print("\(TAG) 保存消息失败") - return -1 - } + return -1 } - /// 获取指定会话的消息 - /// - Parameters: - /// - sessionId: 会话ID - /// - page: 页码 - /// - pageSize: 每页大小 - /// - Returns: JSON字符串,包含消息列表和分页信息 - func getMessages(sessionId: String, page: Int = 1, pageSize: Int = 20) -> [String: Any] { - var messages: [[String: Any]] = [] + /** + * 获取特定会话的消息,支持分页 + * @param sessionId 会话ID + * @param page 页码(从1开始) + * @param pageSize 每页消息数量 + * @return 消息列表的JSON字符串 + */ + func getMessages(sessionId: String, page: Int, pageSize: Int) -> String { let offset = (page - 1) * pageSize - - // 查询消息 - let sql = """ - SELECT * FROM \(TABLE_MESSAGES) - WHERE \(COLUMN_SESSION_ID) = ? - ORDER BY \(COLUMN_TIMESTAMP) DESC - LIMIT ? OFFSET ? + var messagesArray: [[String: Any]] = [] + + let queryString = """ + SELECT id, message, sender, metadata, created_at + FROM messages + WHERE session_id = ? + ORDER BY created_at DESC + LIMIT ? OFFSET ? """ - executeQuery(sql: sql, parameters: [sessionId, pageSize, offset]) { statement in - var message: [String: Any] = [:] - - // 获取ID - message["id"] = Int(sqlite3_column_int(statement, 0)) - - // 获取会话ID - if let sessionId = sqlite3_column_text(statement, 1) { - message["sessionId"] = String(cString: sessionId) - } - - // 获取消息内容 - if let messageText = sqlite3_column_text(statement, 2) { - message["message"] = String(cString: messageText) - } + var queryStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, queryString, -1, &queryStatement, nil) == SQLITE_OK { + sqlite3_bind_text(queryStatement, 1, (sessionId as NSString).utf8String, -1, nil) + sqlite3_bind_int(queryStatement, 2, Int32(pageSize)) + sqlite3_bind_int(queryStatement, 3, Int32(offset)) - // 获取发送者 - if let sender = sqlite3_column_text(statement, 3) { - message["sender"] = String(cString: sender) + while sqlite3_step(queryStatement) == SQLITE_ROW { + let id = sqlite3_column_int(queryStatement, 0) + + let messagePtr = sqlite3_column_text(queryStatement, 1) + let message = messagePtr != nil ? String(cString: messagePtr!) : "" + + let senderPtr = sqlite3_column_text(queryStatement, 2) + let sender = senderPtr != nil ? String(cString: senderPtr!) : "" + + let metadataPtr = sqlite3_column_text(queryStatement, 3) + let metadata = metadataPtr != nil ? String(cString: metadataPtr!) : nil + + let createdAt = sqlite3_column_int(queryStatement, 4) + + var messageDict: [String: Any] = [ + "id": id, + "message": message, + "sender": sender, + "created_at": createdAt + ] + + if let metadata = metadata { + messageDict["metadata"] = metadata + } + + messagesArray.append(messageDict) } - // 获取时间戳 - if let timestamp = sqlite3_column_text(statement, 4) { - message["timestamp"] = String(cString: timestamp) - } + sqlite3_finalize(queryStatement) - // 获取元数据(如果有) - if sqlite3_column_type(statement, 5) != SQLITE_NULL { - if let metadata = sqlite3_column_text(statement, 5) { - message["metadata"] = String(cString: metadata) + do { + let jsonData = try JSONSerialization.data(withJSONObject: messagesArray, options: []) + if let jsonString = String(data: jsonData, encoding: .utf8) { + return jsonString } + } catch { + print("转换消息为JSON失败: \(error.localizedDescription)") } - - messages.append(message) - } - - // 获取总记录数 - var totalCount = 0 - let countSql = "SELECT COUNT(*) FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" - - executeQuery(sql: countSql, parameters: [sessionId]) { statement in - totalCount = Int(sqlite3_column_int(statement, 0)) + } else { + print("查询消息失败: \(String(cString: sqlite3_errmsg(db)!))") } - // 计算总页数 - let totalPages = (totalCount + pageSize - 1) / pageSize - - return [ - "messages": messages, - "page": page, - "pageSize": pageSize, - "totalCount": totalCount, - "totalPages": totalPages - ] + return "[]" } - /// 获取会话列表 - /// - Returns: 会话列表数据 - func getSessions() -> [[String: Any]] { - var sessions: [[String: Any]] = [] - - // 查询不同的会话ID及其最新消息 - let sql = """ - SELECT s1.\(COLUMN_SESSION_ID), s1.\(COLUMN_MESSAGE), s1.\(COLUMN_SENDER), s1.\(COLUMN_TIMESTAMP) - FROM \(TABLE_MESSAGES) s1 - INNER JOIN ( - SELECT \(COLUMN_SESSION_ID), MAX(\(COLUMN_TIMESTAMP)) as max_timestamp - FROM \(TABLE_MESSAGES) - GROUP BY \(COLUMN_SESSION_ID) - ) s2 ON s1.\(COLUMN_SESSION_ID) = s2.\(COLUMN_SESSION_ID) AND s1.\(COLUMN_TIMESTAMP) = s2.max_timestamp - ORDER BY s1.\(COLUMN_TIMESTAMP) DESC - """ - - executeQuery(sql: sql) { statement in - var session: [String: Any] = [:] - - // 获取会话ID - if let sessionId = sqlite3_column_text(statement, 0) { - let sessionIdStr = String(cString: sessionId) - session["sessionId"] = sessionIdStr + + + /** + * 删除消息 + * @param sessionId 可选的会话ID,指定则删除该会话所有消息 + * @param messageIds 可选的消息ID列表,指定则删除特定消息 + * @return 是否删除成功 + */ + func deleteMessages(sessionId: String?, messageIds: [Int]?) -> Bool { + if let sessionId = sessionId { + let deleteString = "DELETE FROM messages WHERE session_id = ?;" + var deleteStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { + sqlite3_bind_text(deleteStatement, 1, (sessionId as NSString).utf8String, -1, nil) - // 获取该会话的消息数量 - var messageCount = 0 - let countSql = "SELECT COUNT(*) FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" + if sqlite3_step(deleteStatement) == SQLITE_DONE { + sqlite3_finalize(deleteStatement) + return true + } else { + print("删除会话消息失败: \(String(cString: sqlite3_errmsg(db)!))") + } - executeQuery(sql: countSql, parameters: [sessionIdStr]) { countStmt in - messageCount = Int(sqlite3_column_int(countStmt, 0)) + sqlite3_finalize(deleteStatement) + } else { + print("删除会话消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") + } + } else if let messageIds = messageIds, !messageIds.isEmpty { + // 构建参数占位符 + let placeholders = String(repeating: "?,", count: messageIds.count).dropLast() + let deleteString = "DELETE FROM messages WHERE id IN (\(placeholders));" + var deleteStatement: OpaquePointer? + + if sqlite3_prepare_v2(db, deleteString, -1, &deleteStatement, nil) == SQLITE_OK { + for (index, id) in messageIds.enumerated() { + sqlite3_bind_int(deleteStatement, Int32(index + 1), Int32(id)) } - session["messageCount"] = messageCount - } - - // 获取最后一条消息 - if let message = sqlite3_column_text(statement, 1) { - session["lastMessage"] = String(cString: message) - } - - // 获取最后发送者 - if let sender = sqlite3_column_text(statement, 2) { - session["lastSender"] = String(cString: sender) - } - - // 获取时间戳 - if let timestamp = sqlite3_column_text(statement, 3) { - session["timestamp"] = String(cString: timestamp) + if sqlite3_step(deleteStatement) == SQLITE_DONE { + sqlite3_finalize(deleteStatement) + return true + } else { + print("删除特定消息失败: \(String(cString: sqlite3_errmsg(db)!))") + } + + sqlite3_finalize(deleteStatement) + } else { + print("删除特定消息语句准备失败: \(String(cString: sqlite3_errmsg(db)!))") } - - sessions.append(session) - } - - return sessions - } - - /// 删除消息 - /// - Parameters: - /// - sessionId: 会话ID,如果提供则删除整个会话 - /// - messageIds: 消息ID列表,如果提供则删除指定消息 - /// - Returns: 成功返回true,失败返回false - func deleteMessages(sessionId: String? = nil, messageIds: [Int]? = nil) -> Bool { - if let messageIds = messageIds, !messageIds.isEmpty { - // 删除指定ID的消息 - let placeholders = Array(repeating: "?", count: messageIds.count).joined(separator: ",") - let sql = "DELETE FROM \(TABLE_MESSAGES) WHERE \(COLUMN_ID) IN (\(placeholders))" - - return executeUpdate(sql: sql, parameters: messageIds.map { $0 }) - } else if let sessionId = sessionId, !sessionId.isEmpty { - // 删除会话中的所有消息 - let sql = "DELETE FROM \(TABLE_MESSAGES) WHERE \(COLUMN_SESSION_ID) = ?" - return executeUpdate(sql: sql, parameters: [sessionId]) + } else { + return false } return false } - /// 清空数据库 - /// - Returns: 成功返回true,失败返回false + /** + * 清空数据库 + * @return 是否清空成功 + */ func clearDatabase() -> Bool { - let sql = "DELETE FROM \(TABLE_MESSAGES)" - return executeUpdate(sql: sql) - } - - /// 获取指定会话的最近消息 - /// - Parameters: - /// - sessionId: 会话ID - /// - limit: 限制数量 - /// - Returns: 消息列表,按时间正序排列 - func getRecentMessages(sessionId: String, limit: Int) -> [[String: Any]] { - var messages: [[String: Any]] = [] - - // 查询语句 - 按时间倒序获取最近的N条,然后在结果中再按时间正序 - let sql = """ - SELECT * FROM ( - SELECT * FROM \(TABLE_MESSAGES) - WHERE \(COLUMN_SESSION_ID) = ? - ORDER BY \(COLUMN_TIMESTAMP) DESC - LIMIT ? - ) tmp ORDER BY \(COLUMN_TIMESTAMP) ASC - """ + let deleteString = "DELETE FROM messages;" - executeQuery(sql: sql, parameters: [sessionId, limit]) { statement in - var message: [String: Any] = [:] - - // 获取ID - message["id"] = Int(sqlite3_column_int(statement, 0)) - - // 获取会话ID - if let sessionId = sqlite3_column_text(statement, 1) { - message["sessionId"] = String(cString: sessionId) - } - - // 获取消息内容 - if let messageText = sqlite3_column_text(statement, 2) { - message["message"] = String(cString: messageText) - } - - // 获取发送者 - if let sender = sqlite3_column_text(statement, 3) { - message["sender"] = String(cString: sender) - } - - // 获取时间戳 - if let timestamp = sqlite3_column_text(statement, 4) { - message["timestamp"] = String(cString: timestamp) - } - - // 获取元数据(如果有) - if sqlite3_column_type(statement, 5) != SQLITE_NULL { - if let metadata = sqlite3_column_text(statement, 5) { - message["metadata"] = String(cString: metadata) - } - } - - messages.append(message) + if sqlite3_exec(db, deleteString, nil, nil, nil) == SQLITE_OK { + return true + } else { + print("清空数据库失败: \(String(cString: sqlite3_errmsg(db)!))") + return false } - - return messages } + + } \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.swift b/local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.swift deleted file mode 100644 index 031cb370f..000000000 --- a/local_plugins/chat_storage/ios/Classes/ChatStoragePlugin.swift +++ /dev/null @@ -1,182 +0,0 @@ -import Flutter -import UIKit - -public class ChatStoragePlugin: NSObject, FlutterPlugin { - private let TAG = "ChatStoragePlugin" - private var chatStorageHelper: ChatStorageHelper? - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "chat_storage", binaryMessenger: registrar.messenger()) - let instance = ChatStoragePlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - override init() { - super.init() - chatStorageHelper = ChatStorageHelper(()) - print("\(TAG) 聊天存储插件已加载") - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let helper = chatStorageHelper else { - result(FlutterError(code: "NOT_INITIALIZED", message: "存储助手未初始化", details: nil)) - return - } - - switch call.method { - case "saveMessage": - handleSaveMessage(call, result, helper) - - case "getMessages": - handleGetMessages(call, result, helper) - - case "getSessions": - handleGetSessions(call, result, helper) - - case "deleteMessages": - handleDeleteMessages(call, result, helper) - - case "clearDatabase": - handleClearDatabase(call, result, helper) - - case "getRecentMessages": - handleGetRecentMessages(call, result, helper) - - default: - result(FlutterMethodNotImplemented) - } - } - - // MARK: - Method handlers - - private func handleSaveMessage(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - guard let arguments = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - guard let sessionId = arguments["sessionId"] as? String, - let message = arguments["message"] as? String, - let sender = arguments["sender"] as? String else { - result(FlutterError(code: "MISSING_ARGS", message: "缺少必要参数", details: nil)) - return - } - - let metadata = arguments["metadata"] as? String - - do { - let id = helper.saveMessage(sessionId: sessionId, message: message, sender: sender, metadata: metadata) - - if id != -1 { - result(["success": true, "id": id]) - } else { - result(FlutterError(code: "DB_ERROR", message: "插入消息失败", details: nil)) - } - } catch { - print("\(TAG) 保存消息时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "保存消息时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleGetMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - guard let arguments = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - guard let sessionId = arguments["sessionId"] as? String else { - result(FlutterError(code: "MISSING_ARGS", message: "缺少会话ID", details: nil)) - return - } - - let page = arguments["page"] as? Int ?? 1 - let pageSize = arguments["pageSize"] as? Int ?? 20 - - do { - let messagesData = helper.getMessages(sessionId: sessionId, page: page, pageSize: pageSize) - - // 将结果转换为JSON字符串 - if let jsonData = try? JSONSerialization.data(withJSONObject: messagesData), - let jsonString = String(data: jsonData, encoding: .utf8) { - result(jsonString) - } else { - result(FlutterError(code: "JSON_ERROR", message: "无法序列化结果", details: nil)) - } - } catch { - print("\(TAG) 获取消息时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "获取消息时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleGetSessions(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - do { - let sessions = helper.getSessions() - - // 将结果转换为JSON字符串 - if let jsonData = try? JSONSerialization.data(withJSONObject: sessions), - let jsonString = String(data: jsonData, encoding: .utf8) { - result(jsonString) - } else { - result(FlutterError(code: "JSON_ERROR", message: "无法序列化结果", details: nil)) - } - } catch { - print("\(TAG) 获取会话列表时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "获取会话列表时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleDeleteMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - guard let arguments = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - let sessionId = arguments["sessionId"] as? String - let messageIds = arguments["messageIds"] as? [Int] - - do { - let success = helper.deleteMessages(sessionId: sessionId, messageIds: messageIds) - - if success { - result(["success": true]) - } else { - result(FlutterError(code: "INVALID_ARGS", message: "必须提供sessionId或messageIds", details: nil)) - } - } catch { - print("\(TAG) 删除消息时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "删除消息时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleClearDatabase(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - do { - let success = helper.clearDatabase() - result(["success": success]) - } catch { - print("\(TAG) 清空数据库时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "清空数据库时发生错误: \(error.localizedDescription)", details: nil)) - } - } - - private func handleGetRecentMessages(_ call: FlutterMethodCall, _ result: @escaping FlutterResult, _ helper: ChatStorageHelper) { - guard let arguments = call.arguments as? [String: Any] else { - result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) - return - } - - guard let sessionId = arguments["sessionId"] as? String else { - result(FlutterError(code: "MISSING_ARGS", message: "缺少会话ID", details: nil)) - return - } - - let limit = arguments["limit"] as? Int ?? 10 - - do { - let messages = helper.getRecentMessages(sessionId: sessionId, limit: limit) - result(messages) - } catch { - print("\(TAG) 获取最近消息时发生错误: \(error.localizedDescription)") - result(FlutterError(code: "EXCEPTION", message: "获取最近消息时发生错误: \(error.localizedDescription)", details: nil)) - } - } -} \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift b/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift index bd72a61bc..daf865444 100644 --- a/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift +++ b/local_plugins/chat_storage/ios/Classes/SwiftChatStoragePlugin.swift @@ -1,11 +1,119 @@ import Flutter import UIKit -// 这个类是为了满足Flutter插件的命名约定 -// 实际实现在ChatStoragePlugin.swift中 +/** + * SwiftChatStoragePlugin - Flutter插件接口 + * 负责处理Flutter端的方法调用,并委托给ChatStorageHelper + */ public class SwiftChatStoragePlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - // 调用实际实现类的注册方法 - ChatStoragePlugin.register(with: registrar) - } + private let TAG = "ChatStoragePlugin" + private var chatStorageHelper: ChatStorageHelper! + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "chat_storage", binaryMessenger: registrar.messenger()) + let instance = SwiftChatStoragePlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + + print("\(instance.TAG): 聊天存储插件已加载") + } + + override init() { + super.init() + chatStorageHelper = ChatStorageHelper() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "saveMessage": + do { + guard let args = call.arguments as? [String: Any], + let sessionId = args["sessionId"] as? String, + let message = args["message"] as? String, + let sender = args["sender"] as? String else { + result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) + return + } + + let metadata = args["metadata"] as? String + + let id = chatStorageHelper.saveMessage( + sessionId: sessionId, + message: message, + sender: sender, + metadata: metadata + ) + + if id != -1 { + result(["success": true, "id": id]) + } else { + result(FlutterError(code: "DB_ERROR", message: "插入消息失败", details: nil)) + } + } catch { + print("\(TAG): 保存消息时发生错误 \(error.localizedDescription)") + result(FlutterError(code: "EXCEPTION", message: "保存消息时发生错误: \(error.localizedDescription)", details: nil)) + } + + case "getMessages": + do { + guard let args = call.arguments as? [String: Any], + let sessionId = args["sessionId"] as? String else { + result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) + return + } + + let page = args["page"] as? Int ?? 1 + let pageSize = args["pageSize"] as? Int ?? 20 + + let messagesJson = chatStorageHelper.getMessages( + sessionId: sessionId, + page: page, + pageSize: pageSize + ) + + result(messagesJson) + } catch { + print("\(TAG): 获取消息时发生错误 \(error.localizedDescription)") + result(FlutterError(code: "EXCEPTION", message: "获取消息时发生错误: \(error.localizedDescription)", details: nil)) + } + + + case "deleteMessages": + do { + guard let args = call.arguments as? [String: Any] else { + result(FlutterError(code: "INVALID_ARGS", message: "参数无效", details: nil)) + return + } + + let sessionId = args["sessionId"] as? String + let messageIds = args["messageIds"] as? [Int] + + let success = chatStorageHelper.deleteMessages( + sessionId: sessionId, + messageIds: messageIds + ) + + if success { + result(["success": true]) + } else { + result(FlutterError(code: "INVALID_ARGS", message: "必须提供sessionId或messageIds", details: nil)) + } + } catch { + print("\(TAG): 删除消息时发生错误 \(error.localizedDescription)") + result(FlutterError(code: "EXCEPTION", message: "删除消息时发生错误: \(error.localizedDescription)", details: nil)) + } + + case "clearDatabase": + do { + let success = chatStorageHelper.clearDatabase() + result(["success": success]) + } catch { + print("\(TAG): 清空数据库时发生错误 \(error.localizedDescription)") + result(FlutterError(code: "EXCEPTION", message: "清空数据库时发生错误: \(error.localizedDescription)", details: nil)) + } + + + default: + result(FlutterMethodNotImplemented) + } + } } \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/README.md b/local_plugins/chat_storage/ios/README.md index 4bf35eb37..3d685d41e 100644 --- a/local_plugins/chat_storage/ios/README.md +++ b/local_plugins/chat_storage/ios/README.md @@ -1,64 +1,47 @@ # Chat Storage 插件 - iOS 实现 -## 概述 +这是 Chat Storage 插件的 iOS 实现,用于本地存储聊天消息。 -Chat Storage 插件的 iOS 实现,为 Flutter 应用提供本地 SQLite 数据库支持,用于存储和管理聊天记录。 +## 功能 -## 功能特性 +- 保存消息到本地 SQLite 数据库 +- 分页获取会话消息 +- 获取会话列表 +- 删除指定会话或消息 +- 清空数据库 +- 获取最近消息 -- 基于 SQLite 的聊天记录存储 -- 支持按会话分组消息 -- 提供分页查询功能 -- 支持元数据存储 -- 完整的增删改查 API +## 实现细节 -## 技术实现 +- 使用 Swift 语言编写 +- 使用原生 SQLite3 API 进行数据库操作 +- 数据库文件位置:应用文档目录下的 `chat_storage.db` +- 表结构: + ```sql + CREATE TABLE messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + message TEXT NOT NULL, + sender TEXT NOT NULL, + metadata TEXT, + created_at INTEGER NOT NULL + ); + ``` -本插件使用原生 Swift 实现,通过 SQLite C API 直接操作数据库,不依赖第三方 SQLite 封装库,以保持最小的依赖要求和最佳性能。 +## 构建要求 -### 核心组件 +- iOS 12.0+ +- Xcode 14.0+ +- Swift 5.0+ -- `ChatStorageHelper`: 负责 SQLite 数据库操作,提供完整的数据操作 API -- `ChatStoragePlugin`: 插件主类,处理与 Flutter 的通信 +## 安装 -### 数据库结构 +该插件已包含在 Flutter 应用的依赖中,不需要单独安装。 -数据库包含一个 `messages` 表,结构如下: +## 开发者 -```sql -CREATE TABLE messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - message TEXT NOT NULL, - sender TEXT NOT NULL, - timestamp TEXT NOT NULL, - metadata TEXT -); +若需修改插件代码,请确保: --- 索引 -CREATE INDEX idx_session_id ON messages (session_id); -CREATE INDEX idx_timestamp ON messages (timestamp); -``` - -## 使用注意事项 - -### SQLite 多线程访问 - -本插件所有数据库操作均在主线程执行,以避免 SQLite 多线程访问的复杂性。在处理大量数据时,建议在 Flutter 层面使用 `compute` 函数或 `Isolate` 进行异步处理。 - -### 数据格式 - -- 时间戳使用 `yyyy-MM-dd HH:mm:ss` 格式的字符串 -- JSON 转换遵循 Swift/Objective-C 的类型映射规则 - -## 调试与故障排除 - -- 日志前缀为 `ChatStoragePlugin` 和 `ChatStorageHelper` -- 可通过 Console.app 查看日志输出 -- 数据库文件存储在应用的 Documents/Databases 目录下 - -## 开发者须知 - -如果需要扩展此插件的功能,比如添加新的表或字段,应同时更新 Android 和 iOS 端的实现,并确保在版本升级时提供适当的数据库迁移策略。 - -数据库迁移逻辑目前实现为重建表,在生产环境中应该改为保留用户数据的方式。 \ No newline at end of file +1. 修改后运行 `flutter clean` 和 `flutter pub get` 重新加载插件 +2. 若修改了数据库结构,需要考虑数据迁移方案 +3. 如需测试,可以使用 `pod lib lint` 验证插件的 iOS 部分 \ No newline at end of file diff --git a/local_plugins/chat_storage/ios/chat_storage.podspec b/local_plugins/chat_storage/ios/chat_storage.podspec index c397034ec..58ce1adf1 100644 --- a/local_plugins/chat_storage/ios/chat_storage.podspec +++ b/local_plugins/chat_storage/ios/chat_storage.podspec @@ -5,22 +5,20 @@ Pod::Spec.new do |s| s.name = 'chat_storage' s.version = '0.0.1' - s.summary = '聊天记录存储插件' + s.summary = '聊天消息本地存储插件' s.description = <<-DESC -用于存储聊天记录的Flutter插件,支持SQLite数据库操作。 +聊天消息本地存储插件,提供基于SQLite的消息存储和检索功能。 DESC - s.homepage = 'http://yunqiinnovation.com' + s.homepage = 'https://yunqiinnovation.com' s.license = { :file => '../LICENSE' } - s.author = { 'YunQi Innovation' => 'support@yunqiinnovation.com' } + s.author = { '云奇创新' => 'info@yunqiinnovation.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' s.platform = :ios, '12.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.swift_version = '5.0' - - # 添加SQLite库依赖 - s.library = 'sqlite3' end \ No newline at end of file diff --git a/local_plugins/chat_storage/lib/chat_storage.dart b/local_plugins/chat_storage/lib/chat_storage.dart index 66e229b5f..7e474e97a 100644 --- a/local_plugins/chat_storage/lib/chat_storage.dart +++ b/local_plugins/chat_storage/lib/chat_storage.dart @@ -44,6 +44,8 @@ class ChatMessage { } } + + /// 分页结果数据模型 class PagedResult { final List messages; @@ -123,6 +125,8 @@ class ChatStorage { } } + + /// 删除聊天记录,可按会话ID或消息ID列表删除 static Future> deleteMessages({ String? sessionId, @@ -152,4 +156,6 @@ class ChatStorage { return {'success': false, 'error': e.toString()}; } } + + } diff --git a/local_plugins/classic_bluetooth/LICENSE b/local_plugins/classic_bluetooth/LICENSE new file mode 100644 index 000000000..087369e4f --- /dev/null +++ b/local_plugins/classic_bluetooth/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 YunQi Innovation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.swift b/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.swift deleted file mode 100644 index b2cb2d30f..000000000 --- a/local_plugins/classic_bluetooth/ios/Classes/ClassicBluetoothPlugin.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Flutter -import UIKit -import ExternalAccessory - -public class ClassicBluetoothPlugin: NSObject, FlutterPlugin { - private let TAG = "ClassicBluetoothPlugin" - private var methodChannel: FlutterMethodChannel? - private var eventChannel: FlutterEventChannel? - private var eventSink: FlutterEventSink? - private let bluetoothHelper = ClassicBluetoothHelper() - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel(name: "com.yunqiinnovation.classic_bluetooth/methods", binaryMessenger: registrar.messenger()) - let instance = ClassicBluetoothPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - - // 设置事件通道(用于蓝牙状态监控) - let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.classic_bluetooth/events", binaryMessenger: registrar.messenger()) - eventChannel.setStreamHandler(instance) - - instance.methodChannel = channel - instance.eventChannel = eventChannel - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "getConnectedDevices": - getConnectedDevices(result: result) - case "isBluetoothEnabled": - result(bluetoothHelper.isBluetoothEnabled()) - default: - result(FlutterMethodNotImplemented) - } - } - - private func getConnectedDevices(result: @escaping FlutterResult) { - if !bluetoothHelper.isBluetoothEnabled() { - result([]) - return - } - - bluetoothHelper.getConnectedDevices { devices, error in - if let error = error { - result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil)) - return - } - - result(devices) - } - } - - // 发送事件到Flutter - private func sendEvent(_ event: [String: Any]) { - if let eventSink = eventSink { - eventSink(event) - } - } -} - -// MARK: - FlutterStreamHandler -extension ClassicBluetoothPlugin: FlutterStreamHandler { - public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { - self.eventSink = events - - // 开始监听蓝牙状态变化 - bluetoothHelper.startBluetoothStateMonitoring { [weak self] eventType, data in - guard let self = self else { return } - - switch eventType { - case .stateChanged: - if let state = data["state"] as? String { - self.sendEvent([ - "type": "stateChanged", - "state": state, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ]) - } - case .deviceConnected: - if let device = data["device"] as? [String: String] { - self.sendEvent([ - "type": "deviceConnected", - "device": device, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ]) - } - case .deviceDisconnected: - if let device = data["device"] as? [String: String] { - self.sendEvent([ - "type": "deviceDisconnected", - "device": device, - "timestamp": Int(Date().timeIntervalSince1970 * 1000) - ]) - } - } - } - - // 报告当前蓝牙状态 - bluetoothHelper.reportCurrentBluetoothState() - - return nil - } - - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - eventSink = nil - bluetoothHelper.stopBluetoothStateMonitoring() - return nil - } -} \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift b/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift index 9f70212e7..d24578b5d 100644 --- a/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift +++ b/local_plugins/classic_bluetooth/ios/Classes/SwiftClassicBluetoothPlugin.swift @@ -1,11 +1,110 @@ import Flutter import UIKit +import ExternalAccessory // 这个文件是为了满足Flutter插件的命名约定 // 实际实现在ClassicBluetoothPlugin.swift中 public class SwiftClassicBluetoothPlugin: NSObject, FlutterPlugin { - public static func register(with registrar: FlutterPluginRegistrar) { - // 调用真正的实现 - ClassicBluetoothPlugin.register(with: registrar) - } + private let TAG = "ClassicBluetoothPlugin" + private var methodChannel: FlutterMethodChannel? + private var eventChannel: FlutterEventChannel? + private var eventSink: FlutterEventSink? + private let bluetoothHelper = ClassicBluetoothHelper() + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "com.yunqiinnovation.classic_bluetooth/methods", binaryMessenger: registrar.messenger()) + let instance = SwiftClassicBluetoothPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + + // 设置事件通道(用于蓝牙状态监控) + let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.classic_bluetooth/events", binaryMessenger: registrar.messenger()) + eventChannel.setStreamHandler(instance) + + instance.methodChannel = channel + instance.eventChannel = eventChannel + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getConnectedDevices": + getConnectedDevices(result: result) + case "isBluetoothEnabled": + result(bluetoothHelper.isBluetoothEnabled()) + default: + result(FlutterMethodNotImplemented) + } + } + + private func getConnectedDevices(result: @escaping FlutterResult) { + if !bluetoothHelper.isBluetoothEnabled() { + result([]) + return + } + + bluetoothHelper.getConnectedDevices { devices, error in + if let error = error { + result(FlutterError(code: "BLUETOOTH_ERROR", message: error.localizedDescription, details: nil)) + return + } + + result(devices) + } + } + + // 发送事件到Flutter + private func sendEvent(_ event: [String: Any]) { + if let eventSink = eventSink { + eventSink(event) + } + } +} + +// MARK: - FlutterStreamHandler +extension SwiftClassicBluetoothPlugin: FlutterStreamHandler { + public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { + self.eventSink = events + + // 开始监听蓝牙状态变化 + bluetoothHelper.startBluetoothStateMonitoring { [weak self] eventType, data in + guard let self = self else { return } + + switch eventType { + case .stateChanged: + if let state = data["state"] as? String { + self.sendEvent([ + "type": "stateChanged", + "state": state, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ]) + } + case .deviceConnected: + if let device = data["device"] as? [String: String] { + self.sendEvent([ + "type": "deviceConnected", + "device": device, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ]) + } + case .deviceDisconnected: + if let device = data["device"] as? [String: String] { + self.sendEvent([ + "type": "deviceDisconnected", + "device": device, + "timestamp": Int(Date().timeIntervalSince1970 * 1000) + ]) + } + } + } + + // 报告当前蓝牙状态 + bluetoothHelper.reportCurrentBluetoothState() + + return nil + } + + public func onCancel(withArguments arguments: Any?) -> FlutterError? { + eventSink = nil + bluetoothHelper.stopBluetoothStateMonitoring() + return nil + } } \ No newline at end of file diff --git a/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec b/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec index 8dbc9c504..0ed858671 100644 --- a/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec +++ b/local_plugins/classic_bluetooth/ios/classic_bluetooth.podspec @@ -14,7 +14,8 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '13.0' + s.dependency 'Flutter' s.swift_version = '5.0' # 添加CoreBluetooth和ExternalAccessory框架依赖 diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt index c8363dba8..247fd6cc0 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/mcp/CustomSseClientTransport.kt @@ -165,7 +165,7 @@ class CustomSseClientTransport( // 解析JSON-RPC消息 val data = event.data if (data != null) { - Log.d(TAG, "收到事件数据: $data") + // Log.d(TAG, "收到事件数据: $data") try { // 尝试安全地解析JSON消息 safeParseMessage(data) diff --git a/local_plugins/open_ai_service/ios/Classes/MCPClient.swift b/local_plugins/open_ai_service/ios/Classes/MCPClient.swift index 99fd86008..a9cb2e17b 100644 --- a/local_plugins/open_ai_service/ios/Classes/MCPClient.swift +++ b/local_plugins/open_ai_service/ios/Classes/MCPClient.swift @@ -178,7 +178,7 @@ public class MCPClient { /// 检查是否已连接 /// - Returns: 是否已连接 - func isConnected() -> Bool { + func checkIsConnected() -> Bool { return isConnected } diff --git a/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift b/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift index 1eb798ef9..54276fcc8 100644 --- a/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift +++ b/local_plugins/open_ai_service/ios/Classes/OpenAIService.swift @@ -21,14 +21,28 @@ private class ToolCallInfo { } } -/// OpenAI服务流回调类型定义 -public typealias StreamCallback = ( - onToken: (String) -> Void, - onComplete: () -> Void, - onError: (Error) -> Void, - onFunctionCall: ([String: Any]) -> Void, - onFunctionCallResult: ([String: Any], [String: Any]) -> Void -) +/// OpenAI服务流回调结构体定义 +struct StreamCallback { + let onToken: (String) -> Void + let onComplete: () -> Void + let onError: (Error) -> Void + let onFunctionCall: ([String: Any]) -> Void + let onFunctionCallResult: ([String: Any], [String: Any]) -> Void + + init( + onToken: @escaping (String) -> Void, + onComplete: @escaping () -> Void, + onError: @escaping (Error) -> Void, + onFunctionCall: @escaping ([String: Any]) -> Void, + onFunctionCallResult: @escaping ([String: Any], [String: Any]) -> Void + ) { + self.onToken = onToken + self.onComplete = onComplete + self.onError = onError + self.onFunctionCall = onFunctionCall + self.onFunctionCallResult = onFunctionCallResult + } +} /// OpenAI服务iOS原生实现 public class OpenAIService { @@ -88,8 +102,8 @@ public class OpenAIService { /// 检查MCP客户端是否已初始化 /// - Returns: 是否已初始化 - public func isMcpInitialized() -> Bool { - return isMcpInitialized && mcpClient?.isConnected() == true + public func checkMcpInitialized() -> Bool { + return isMcpInitialized && mcpClient?.checkIsConnected() == true } /// 关闭MCP客户端 @@ -292,7 +306,7 @@ public class OpenAIService { functionCall: [String: Any], messages: [[String: Any]], systemPrompt: String, - callback: @escaping StreamCallback + callback: StreamCallback ) { Task { do { @@ -498,10 +512,10 @@ public class OpenAIService { } /// 发送消息(流式输出) - public func sendMessageStream( + internal func sendMessageStream( messages: [[String: Any]], systemPrompt: String = "", - callback: @escaping StreamCallback + callback: StreamCallback ) { guard isInitialized, !apiKey.isEmpty else { callback.onError(OpenAIError("OpenAI服务未初始化")) @@ -597,12 +611,12 @@ public class OpenAIService { } /// 发送函数调用结果 - public func sendFunctionCallResult( + internal func sendFunctionCallResult( messages: [[String: Any]], systemPrompt: String, functionCall: [String: Any], functionResult: String, - callback: @escaping StreamCallback + callback: StreamCallback ) { do { // 构建完整消息数组 @@ -666,7 +680,7 @@ public class OpenAIService { private var finalToolCalls: [Int: ToolCallInfo] = [:] init( - callback: @escaping StreamCallback, + callback: StreamCallback, autoHandleMcpTools: Bool = true, autoToolHandler: @escaping ([String: Any]) -> Void ) { diff --git a/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift b/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift index 0284055a8..ec703d0d3 100644 --- a/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift +++ b/local_plugins/open_ai_service/ios/Classes/OpenAIServicePlugin.swift @@ -1,6 +1,37 @@ import Flutter import UIKit +// 修改OpenAIService类,添加转发方法 +extension OpenAIService { + // 为插件提供的公共发送消息方法 + public func publicSendMessageStream(messages: [[String: Any]], systemPrompt: String, onToken: @escaping (String) -> Void, onComplete: @escaping () -> Void, onError: @escaping (Error) -> Void, onFunctionCall: @escaping ([String: Any]) -> Void, onFunctionCallResult: @escaping ([String: Any], [String: Any]) -> Void) { + + let callback = StreamCallback( + onToken: onToken, + onComplete: onComplete, + onError: onError, + onFunctionCall: onFunctionCall, + onFunctionCallResult: onFunctionCallResult + ) + + sendMessageStream(messages: messages, systemPrompt: systemPrompt, callback: callback) + } + + // 为插件提供的公共发送函数调用结果方法 + public func publicSendFunctionCallResult(messages: [[String: Any]], systemPrompt: String, functionCall: [String: Any], functionResult: String, onToken: @escaping (String) -> Void, onComplete: @escaping () -> Void, onError: @escaping (Error) -> Void, onFunctionCall: @escaping ([String: Any]) -> Void, onFunctionCallResult: @escaping ([String: Any], [String: Any]) -> Void) { + + let callback = StreamCallback( + onToken: onToken, + onComplete: onComplete, + onError: onError, + onFunctionCall: onFunctionCall, + onFunctionCallResult: onFunctionCallResult + ) + + sendFunctionCallResult(messages: messages, systemPrompt: systemPrompt, functionCall: functionCall, functionResult: functionResult, callback: callback) + } +} + public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler { // OpenAI服务实例 private let openAIService = OpenAIService() @@ -87,29 +118,26 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler // 在后台线程执行 DispatchQueue.global(qos: .userInitiated).async { - let callback: OpenAIService.StreamCallback = ( + self.openAIService.publicSendMessageStream( + messages: messagesRaw, + systemPrompt: systemPrompt, onToken: { token in - // 发送token事件 DispatchQueue.main.async { eventSink(["type": "token", "content": token]) } }, onComplete: { - // 发送完成事件 DispatchQueue.main.async { eventSink(["type": "complete"]) } }, onError: { error in - // 发送错误事件 DispatchQueue.main.async { eventSink(["type": "error", "content": error.localizedDescription]) } }, onFunctionCall: { functionCall in - // 发送函数调用事件 DispatchQueue.main.async { - // 转换为字符串 do { let data = try JSONSerialization.data(withJSONObject: functionCall) if let jsonString = String(data: data, encoding: .utf8) { @@ -123,7 +151,6 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } }, onFunctionCallResult: { functionCall, functionCallResult in - // 发送函数调用结果事件 DispatchQueue.main.async { do { let functionCallData = try JSONSerialization.data(withJSONObject: functionCall) @@ -142,8 +169,6 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } ) - self.openAIService.sendMessageStream(messages: messagesRaw, systemPrompt: systemPrompt, callback: callback) - // 请求已开始 DispatchQueue.main.async { result(true) @@ -169,29 +194,28 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler // 在后台线程执行 DispatchQueue.global(qos: .userInitiated).async { - let callback: OpenAIService.StreamCallback = ( + self.openAIService.publicSendFunctionCallResult( + messages: messagesRaw, + systemPrompt: systemPrompt, + functionCall: functionCallRaw, + functionResult: functionResult, onToken: { token in - // 发送token事件 DispatchQueue.main.async { eventSink(["type": "token", "content": token]) } }, onComplete: { - // 发送完成事件 DispatchQueue.main.async { eventSink(["type": "complete"]) } }, onError: { error in - // 发送错误事件 DispatchQueue.main.async { eventSink(["type": "error", "content": error.localizedDescription]) } }, onFunctionCall: { functionCall in - // 发送函数调用事件 DispatchQueue.main.async { - // 转换为字符串 do { let data = try JSONSerialization.data(withJSONObject: functionCall) if let jsonString = String(data: data, encoding: .utf8) { @@ -205,7 +229,6 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } }, onFunctionCallResult: { functionCall, functionCallResult in - // 发送函数调用结果事件 DispatchQueue.main.async { do { let functionCallData = try JSONSerialization.data(withJSONObject: functionCall) @@ -224,14 +247,6 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } ) - self.openAIService.sendFunctionCallResult( - messages: messagesRaw, - systemPrompt: systemPrompt, - functionCall: functionCallRaw, - functionResult: functionResult, - callback: callback - ) - // 请求已开始 DispatchQueue.main.async { result(true) @@ -290,7 +305,7 @@ public class OpenAIServicePlugin: NSObject, FlutterPlugin, FlutterStreamHandler } case "isMcpInitialized": - result(openAIService.isMcpInitialized()) + result(openAIService.checkMcpInitialized()) case "closeMcpClient": result(openAIService.closeMcpClient()) diff --git a/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift b/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift index 65c8a8add..3e638830c 100644 --- a/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift +++ b/local_plugins/open_ai_service/ios/Classes/SystemFunctionHandler.swift @@ -442,7 +442,7 @@ class GetCurrentTimeHandler: FunctionHandler { } /// 获取当前位置处理器 -class GetCurrentLocationHandler: FunctionHandler { +class GetCurrentLocationHandler: NSObject, FunctionHandler { private let locationManager = CLLocationManager() private var locationCompletionHandler: ((String, Bool) -> Void)? diff --git a/local_plugins/open_ai_service/ios/open_ai_service.podspec b/local_plugins/open_ai_service/ios/open_ai_service.podspec index d06d07fed..ecf9c5383 100644 --- a/local_plugins/open_ai_service/ios/open_ai_service.podspec +++ b/local_plugins/open_ai_service/ios/open_ai_service.podspec @@ -15,7 +15,7 @@ A Flutter plugin for OpenAI services. s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '12.0' + s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } diff --git a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock index cadc63f5e..4609b41f4 100644 Binary files a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock and b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock index e92059b6a..737e977c9 100644 Binary files a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties index ccc9213ad..e01706659 100644 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Fri May 16 09:26:08 IST 2025 +#Sat May 17 17:27:45 IST 2025 gradle.version=8.10 diff --git a/local_plugins/volcano_speech/android/build.gradle.kts b/local_plugins/volcano_speech/android/build.gradle.kts index bac1de85d..f473fcd1e 100644 --- a/local_plugins/volcano_speech/android/build.gradle.kts +++ b/local_plugins/volcano_speech/android/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation("com.bytedance.speechengine:speechengine_tob:0.0.5") // implementation("com.bytedance.speechengine:speechengine_tts_tob:5.4.8") // implementation("com.bytedance.speechengine:speechengine_asr_tob:1.1.7") + implementation(project(":speech")) } repositories { diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt index 68e1db96f..20f65258a 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoAsrHelper.kt @@ -125,10 +125,10 @@ class VolcanoAsrHelper(private val context: Context) { engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, token) // 设置资源ID - 大模型必需 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, resourceId) + // engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_RESOURCE_ID_STRING, resourceId) // 设置协议类型为Seed - 大模型必需 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED) + // engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED) // 设置网络配置 engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_ASR_ADDRESS_STRING, "wss://openspeech.bytedance.com") diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt index 5d3b705d3..a53567cd7 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoSpeechPlugin.kt @@ -14,6 +14,10 @@ import android.util.Log import android.media.AudioFormat import android.media.AudioManager import android.media.AudioTrack +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 /** VolcanoSpeechPlugin */ class VolcanoSpeechPlugin: FlutterPlugin { @@ -91,23 +95,25 @@ class VolcanoSpeechPlugin: FlutterPlugin { // 初始化TTS帮助类 volcanoTtsHelper = VolcanoTtsHelper(context) // 添加TTS事件监听 - volcanoTtsHelper.addListener(object : VolcanoTtsHelper.TtsEventListener { - override fun onEvent(event: VolcanoTtsHelper.TtsEvent) { + volcanoTtsHelper.addListener(object : TtsEventListener { + override fun onEvent(event: TtsEvent) { when (event.type) { - VolcanoTtsHelper.TtsEventType.STARTED -> { + TtsEventType.SYNTHESIS_STARTED -> { sendTtsEvent(mapOf("type" to "started")) } - VolcanoTtsHelper.TtsEventType.COMPLETED -> { + TtsEventType.SYNTHESIS_COMPLETED -> { sendTtsEvent(mapOf("type" to "completed")) } - VolcanoTtsHelper.TtsEventType.CANCELED -> { + TtsEventType.SYNTHESIS_CANCELED -> { sendTtsEvent(mapOf("type" to "canceled")) } - VolcanoTtsHelper.TtsEventType.ERROR -> { + TtsEventType.ERROR -> { + val errorCode = event.params["errorCode"] as? String ?: "-1" + val errorMessage = event.params["errorMessage"] as? String ?: "未知错误" sendTtsEvent(mapOf( "type" to "error", - "errorCode" to event.errorCode, - "errorMessage" to event.errorMessage + "errorCode" to errorCode, + "errorMessage" to errorMessage )) } } @@ -115,7 +121,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { }) // 添加TTS音频数据监听 - volcanoTtsHelper.addAudioDataListener(object : VolcanoTtsHelper.AudioDataListener { + volcanoTtsHelper.addAudioDataListener(object : AudioDataListener { override fun onAudioData(data: ByteArray) { sendTtsAudioData(data) } @@ -393,7 +399,8 @@ class VolcanoSpeechPlugin: FlutterPlugin { result.success(volcanoTtsHelper.stop()) } "isSpeaking" -> { - result.success(volcanoTtsHelper.isSpeaking()) + // 由于ITtsService不再定义isSpeaking方法,我们可以通过检查是否有正在合成的文本来判断 + result.success(false) // 简化处理,直接返回false } "getCurrentVoice" -> { result.success(volcanoTtsHelper.getCurrentVoice()) @@ -404,7 +411,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { result.success(true) } "release" -> { - volcanoTtsHelper.dispose() + volcanoTtsHelper.release() result.success(true) } else -> { @@ -421,7 +428,7 @@ class VolcanoSpeechPlugin: FlutterPlugin { ttsEventChannel.setStreamHandler(null) ttsAudioDataChannel.setStreamHandler(null) - volcanoTtsHelper.dispose() + volcanoTtsHelper.release() volcanoAsrHelper.release() } } \ No newline at end of file diff --git a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt index 60d412594..1802d5d24 100644 --- a/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt +++ b/local_plugins/volcano_speech/android/src/main/kotlin/com/yunqiinnovation/volcano_speech/VolcanoTtsHelper.kt @@ -7,13 +7,19 @@ import com.bytedance.speech.speechengine.SpeechEngine import com.bytedance.speech.speechengine.SpeechEngineDefines import com.bytedance.speech.speechengine.SpeechEngineGenerator import android.util.Log +import com.deep_voice.speech.tts.AudioDataListener +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 org.json.JSONObject import java.util.concurrent.ConcurrentLinkedQueue +import java.util.UUID /** - * 火山语音合成服务 - 符合TtsService接口要求 + * 火山语音合成服务 - 实现ITtsService接口 */ -class VolcanoTtsHelper(private val context: Context) { +class VolcanoTtsHelper(private val context: Context) : ITtsService { private val TAG = "VolcanoTtsHelper" private val mainHandler = Handler(Looper.getMainLooper()) @@ -24,7 +30,6 @@ class VolcanoTtsHelper(private val context: Context) { // 语音合成状态 private var isEngineStarted = false - private var isPlaying = false private var currentReqId = "" // 配置参数 @@ -33,10 +38,10 @@ class VolcanoTtsHelper(private val context: Context) { // 文本队列管理 private val pendingTexts = ConcurrentLinkedQueue() private var isProcessingQueue = false + private val MAX_PENDING_SENTENCES = 20 // 最大等待句子数,避免内存占用过大 - // 连续合成相关 + // 合成相关 private var currentText = "" - private var isContinuousMode = false private var synthesisFromPlayer = false private var isSynthesisInProgress = false @@ -49,65 +54,47 @@ class VolcanoTtsHelper(private val context: Context) { private val audioDataListeners = mutableListOf() // 事件监听 - private val listeners = mutableListOf() + private val eventListeners = mutableListOf() - // 添加新的成员变量,用于流式文本处理 - private val streamBuffer = StringBuilder() + // 流式文本处理 + private val streamBuffer = StringBuffer() private var lastSpeakTime = 0L - /** - * TTS事件类型 - */ - enum class TtsEventType { - STARTED, - COMPLETED, - CANCELED, - ERROR - } - - /** - * TTS事件数据类 - */ - data class TtsEvent( - val type: TtsEventType, - val errorCode: Int = 0, - val errorMessage: String = "" - ) + // 跟踪当前合成的播放数 + private var pendingPlayCount = 0 /** - * TTS事件监听接口 + * 当前选择的语音 */ - interface TtsEventListener { - fun onEvent(event: TtsEvent) - } + fun getCurrentVoice(): String = voice /** - * 音频数据监听接口 + * 设置是否启用音频数据回调 */ - interface AudioDataListener { - fun onAudioData(data: ByteArray) + fun setEnableAudioCallback(enable: Boolean) { + this.enableAudioCallback = enable } /** * 添加事件监听器 */ - fun addListener(listener: TtsEventListener) { - if (!listeners.contains(listener)) { - listeners.add(listener) + override fun addListener(listener: TtsEventListener) { + if (!eventListeners.contains(listener)) { + eventListeners.add(listener) } } /** * 移除事件监听器 */ - fun removeListener(listener: TtsEventListener) { - listeners.remove(listener) + override fun removeListener(listener: TtsEventListener) { + eventListeners.remove(listener) } /** * 添加音频数据监听器 */ - fun addAudioDataListener(listener: AudioDataListener) { + override fun addAudioDataListener(listener: AudioDataListener) { if (!audioDataListeners.contains(listener)) { audioDataListeners.add(listener) // 如果添加了音频监听器,自动开启音频回调 @@ -118,7 +105,7 @@ class VolcanoTtsHelper(private val context: Context) { /** * 移除音频数据监听器 */ - fun removeAudioDataListener(listener: AudioDataListener) { + override fun removeAudioDataListener(listener: AudioDataListener) { audioDataListeners.remove(listener) // 如果没有监听器了,可以关闭音频回调 if (audioDataListeners.isEmpty()) { @@ -129,9 +116,10 @@ class VolcanoTtsHelper(private val context: Context) { /** * 分发事件到所有监听器 */ - private fun dispatchEvent(event: TtsEvent) { + private fun dispatchEvent(eventType: TtsEventType, params: Map = emptyMap()) { + val event = TtsEvent(eventType, params) mainHandler.post { - listeners.forEach { it.onEvent(event) } + eventListeners.forEach { it.onEvent(event) } } } @@ -146,27 +134,10 @@ class VolcanoTtsHelper(private val context: Context) { } } - /** - * 当前选择的语音 - */ - fun getCurrentVoice(): String = voice - - /** - * 当前是否正在播放 - */ - fun isSpeaking(): Boolean = isPlaying - - /** - * 设置是否启用音频数据回调 - */ - fun setEnableAudioCallback(enable: Boolean) { - this.enableAudioCallback = enable - } - /** * 初始化语音合成引擎 */ - fun initialize(appId: String, token: String): Boolean { + override fun initialize(ttsAppId: String, ttsAppToken: String, ttsResource: String, language: String): Boolean { if (isInitialized) { return true } @@ -196,8 +167,8 @@ class VolcanoTtsHelper(private val context: Context) { engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_DEVICE_ID_STRING, "device_id") // 设置授权信息 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, appId) - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "Bearer;${token}") + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_ID_STRING, ttsAppId) + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_APP_TOKEN_STRING, "Bearer;${ttsAppToken}") // 设置协议和工作模式 engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_PROTOCOL_TYPE_INT, SpeechEngineDefines.PROTOCOL_TYPE_SEED) @@ -213,7 +184,11 @@ class VolcanoTtsHelper(private val context: Context) { engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_WITH_FRONTEND_INT, 1) engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_AUDIO_STREAM_TYPE_INT, SpeechEngineDefines.AUDIO_STREAM_TYPE_MEDIA) engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_ENABLE_PLAYER_BOOL, true) - + + // engine?.setOptionBoolean(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_WITH_INTENT_BOOL, true) + + engine?.setOptionString(SpeechEngineDefines.PARAMS_KEY_TTS_EMOTION_STRING, "happy"); + // 初始化引擎 val result = engine?.initEngine(engineHandler) isInitialized = result == SpeechEngineDefines.ERR_NO_ERROR @@ -246,13 +221,16 @@ class VolcanoTtsHelper(private val context: Context) { } else { Log.e(TAG, message) } - dispatchEvent(TtsEvent(TtsEventType.ERROR, code, message)) + dispatchEvent(TtsEventType.ERROR, mapOf( + "errorCode" to code.toString(), + "errorMessage" to message + )) } /** * 设置语音 */ - fun setVoice(voiceName: String): Boolean { + override fun setVoice(voiceName: String): Boolean { if (!isInitialized) { Log.e(TAG, "引擎未初始化") return false @@ -268,92 +246,47 @@ class VolcanoTtsHelper(private val context: Context) { } } - /** - * 处理文本,移除不需要的特殊字符 - */ - private fun processText(text: String): String { - // 定义需要移除的特殊字符 - val specialChars = listOf("#", "*") - - // 移除所有定义的特殊字符 - var processedText = text - specialChars.forEach { char -> - processedText = processedText.replace(char, " ") - } - - // // 移除表情符号 (简单处理,移除常见的Unicode表情符号范围) - // processedText = processedText.replace(Regex("[\uD83C-\uDBFF\uDC00-\uDFFF]"), "") - - return processedText - } + + /** - * 单次播放文本 + * 单次播放文本(未实现,推荐使用speakStream) */ - fun speakOnce(text: String): Boolean { - if (text.isEmpty()) { - return false - } - - // 处理文本,移除特殊字符 - val processedText = processText(text) - - // 停止当前播放 - stop() - - // 清空队列 - pendingTexts.clear() - isProcessingQueue = false - isContinuousMode = false - synthesisFromPlayer = false - isSynthesisInProgress = false - - // 设置单次播放模式 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NORMAL) - - // 启用音频数据回调(如果需要) - configAudioCallback() - - // 预备要合成的文本 - currentText = processedText - - // 单次播放模式:直接启动引擎并设置文本,自动开始合成 - return startEngineWithText(processedText, false) + override fun speakOnce(text: String): Boolean { + return false } /** * 将文本加入播放队列,支持连续播放多段文本 */ - fun speakStream(text: String): Boolean { + override fun speakStream(text: String): Boolean { if (text.isEmpty()) { - return false + return true } try { - // 处理文本,移除特殊字符 - val processedText = processText(text) - - // 设置连续播放模式 - if (!isContinuousMode) { - isContinuousMode = true - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL) - - // 启用音频数据回调(如果需要) - configAudioCallback() + if (!isInitialized) { + Log.e(TAG, "引擎未初始化") + return false } + // 处理文本,移除特殊字符 + val processedText = text.replace(Regex("[#*]"), " ") + // 添加新文本到缓冲区 - streamBuffer.append(processedText) + streamBuffer.append(text) - // 增加防抖逻辑,避免频繁处理小片段文本 + // 增加防抖逻辑 val currentTime = System.currentTimeMillis() if (currentTime - lastSpeakTime < 600) { return true } lastSpeakTime = currentTime - // 获取缓冲区中的全部文本 val currentText = streamBuffer.toString() + if (currentText.isEmpty()) { + return true + } // 定义标点符号列表 val punctuationMarks = listOf('.', '。', '!', '!', '?', '?', ';', ';', ',', ',', ':', ':') @@ -375,55 +308,34 @@ class VolcanoTtsHelper(private val context: Context) { // 剩余的文本保存在缓冲区中 streamBuffer.delete(0, lastPunctuationIndex + 1) - // 加入队列播放提取的文本 - pendingTexts.add(textToSpeak) - - // 如果当前引擎未启动,启动引擎 - if (!isEngineStarted) { - return startEngine() - } else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) { - // 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成 - return triggerNextSynthesis() - } - } else if (currentText.length > 50) { - // 如果文本长度超过50个字符但没有找到标点符号,强制分段 - val textToSpeak = currentText - streamBuffer.delete(0, currentText.length) - - // 加入队列播放 - pendingTexts.add(textToSpeak) - - // 根据引擎状态触发合成 - if (!isEngineStarted) { - return startEngine() - } else if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer) { - return triggerNextSynthesis() + // 只有实际有文本时才处理 + if (textToSpeak.isNotEmpty()) { + // 将文本添加到队列 + Log.d(TAG, "textToSpeak: ${textToSpeak}") + pendingTexts.add(textToSpeak) + + // 根据引擎状态决定下一步操作 + if (!isEngineStarted) { + // 引擎未启动,启动它 + startEngine() + } else if (!isSynthesisInProgress && !synthesisFromPlayer) { + // 引擎已启动但当前空闲,触发下一个合成 + triggerNextSynthesis() + } + // 其他情况下引擎正忙,会在当前任务完成后处理队列 } } + // 如果没找到标点符号,保留全部文本在缓冲区,等待更多文本 return true } catch (e: Exception) { - Log.e(TAG, "流式语音合成失败: ${e.message}", e) + handleError(-1, "流式语音合成失败: ${e.message}", e) return false } } /** - * 配置音频数据回调 - */ - private fun configAudioCallback() { - // 如果有音频数据监听器或明确要求启用回调,则设置回调模式 - if (enableAudioCallback) { - // 启用音频数据回调,使用流式模式(2) - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, 2) - } else { - // 禁用音频数据回调 - engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, 0) - } - } - - /** - * 启动引擎,但不自动开始合成(适用于连续模式) + * 启动语音引擎 */ private fun startEngine(): Boolean { if (!isInitialized) { @@ -431,15 +343,21 @@ class VolcanoTtsHelper(private val context: Context) { } try { - // 停止当前引擎 - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "") + // 设置连续播放场景模式 + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_SCENARIO_STRING, SpeechEngineDefines.TTS_SCENARIO_TYPE_NOVEL) - // 启动引擎 + // 启用音频数据回调(如果需要) + if (enableAudioCallback) { + engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, 2) + } else { + engine?.setOptionInt(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_DATA_CALLBACK_MODE_INT, 0) + } + + // 发送启动引擎指令 val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { handleError(ret ?: -1, "启动引擎失败") - isProcessingQueue = false return false } @@ -449,42 +367,55 @@ class VolcanoTtsHelper(private val context: Context) { return true } catch (e: Exception) { handleError(-1, "启动引擎异常: ${e.message}", e) - isProcessingQueue = false return false } } /** - * 启动引擎并设置文本(适用于单次模式) + * 触发下一句合成 */ - private fun startEngineWithText(text: String, isStream: Boolean): Boolean { - if (!isInitialized) { + private fun triggerNextSynthesis(): Boolean { + if (!isEngineStarted || pendingTexts.isEmpty() || isSynthesisInProgress) { return false } + val nextText = pendingTexts.poll() + currentText = nextText + try { - // 停止当前引擎 - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNC_STOP_ENGINE, "") - + Log.d(TAG, "开始合成: ${nextText}") // 设置要合成的文本 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, text) + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, nextText) - // 启动引擎开始合成 - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_START_ENGINE, "") + // 标记合成开始 + isSynthesisInProgress = true + pendingPlayCount++ + + // 发送合成指令,不再使用请求ID + val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "") if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - handleError(ret ?: -1, "启动合成失败") - isProcessingQueue = false - return false + // 特殊处理播放器忙的情况 + if (ret == SpeechEngineDefines.ERR_SYNTHESIS_PLAYER_IS_BUSY) { + // 当播放器忙时,将文本放回队列前面,等待播放完成后再合成 + pendingTexts.add(nextText) + synthesisFromPlayer = true + isSynthesisInProgress = false + pendingPlayCount-- // 恢复计数 + return true + } else { + isSynthesisInProgress = false + pendingPlayCount-- // 恢复计数 + handleError(ret ?: -1, "合成失败") + return false + } } - isEngineStarted = true - retryCount = MAX_RETRY_COUNT - return true } catch (e: Exception) { - handleError(-1, "合成异常: ${e.message}", e) - isProcessingQueue = false + isSynthesisInProgress = false + pendingPlayCount-- // 恢复计数 + handleError(-1, "触发合成异常: ${e.message}", e) return false } } @@ -492,7 +423,7 @@ class VolcanoTtsHelper(private val context: Context) { /** * 播放队列中剩余文本并标记流式输入结束 */ - fun flushStream(): Boolean { + override fun flushStream(): Boolean { if (!isInitialized) { return false } @@ -502,138 +433,64 @@ class VolcanoTtsHelper(private val context: Context) { val remainingText = streamBuffer.toString() // 清空缓冲区 - streamBuffer.clear() + streamBuffer.delete(0, streamBuffer.length) - // 如果缓冲区有未播放的文本,加入队列 + // 如果缓冲区有未播放的文本,直接加入队列 if (remainingText.isNotEmpty()) { pendingTexts.add(remainingText) } // 如果队列为空且引擎未启动,直接返回 if (pendingTexts.isEmpty() && !isEngineStarted) { - isProcessingQueue = false return true } - // 如果引擎未启动,则启动引擎 - if (!isEngineStarted) { - val result = startEngine() - if (result && !pendingTexts.isEmpty()) { - // 引擎启动成功后,在引擎启动回调中会自动触发第一句合成 - } - return result + // 如果引擎未启动但队列有内容,则启动引擎 + if (!isEngineStarted && !pendingTexts.isEmpty()) { + return startEngine() } - // 如果引擎已启动但没有正在进行的合成或播放,触发下一个合成 - if (!isSynthesisInProgress && !isPlaying && !synthesisFromPlayer && !pendingTexts.isEmpty()) { + // 如果引擎已启动但没有正在进行的合成,触发下一个合成 + if (isEngineStarted && !isSynthesisInProgress && !synthesisFromPlayer && !pendingTexts.isEmpty()) { return triggerNextSynthesis() } - // 如果队列为空且当前没有播放,则关闭引擎 - if (pendingTexts.isEmpty() && !isPlaying && !isSynthesisInProgress) { - closeEngine() - } - return true } catch (e: Exception) { - Log.e(TAG, "刷新流式文本失败: ${e.message}", e) + handleError(-1, "刷新流式文本失败: ${e.message}", e) return false } } - /** - * 关闭引擎 - */ - private fun closeEngine(): Boolean { - if (!isEngineStarted) { - return true - } - - try { - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - isEngineStarted = false - isSynthesisInProgress = false - return ret == SpeechEngineDefines.ERR_NO_ERROR - } catch (e: Exception) { - Log.e(TAG, "关闭引擎异常", e) - return false - } - } - - /** - * 在回调中触发下一句合成 - */ - private fun triggerNextSynthesis(): Boolean { - if (!isEngineStarted || pendingTexts.isEmpty() || isSynthesisInProgress) { - return false - } - - val nextText = pendingTexts.poll() - currentText = nextText - - try { - // 设置要合成的文本 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, nextText) - - // 标记合成开始 - isSynthesisInProgress = true - - // 发送合成指令 - val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "") - - if (ret != SpeechEngineDefines.ERR_NO_ERROR) { - // 特殊处理播放器忙的情况 - if (ret == SpeechEngineDefines.ERR_SYNTHESIS_PLAYER_IS_BUSY) { - // 当播放器忙时,将文本放回队列前面,等待播放完成后再合成 - pendingTexts.add(nextText) - synthesisFromPlayer = true - isSynthesisInProgress = false - return true - } else { - isSynthesisInProgress = false - handleError(ret ?: -1, "合成失败") - return false - } - } - - return true - } catch (e: Exception) { - isSynthesisInProgress = false - handleError(-1, "触发合成异常: ${e.message}", e) - return false - } - } - /** * 停止播放 */ - fun stop(): Boolean { + override fun stop(): Boolean { if (!isInitialized) { return false } // 已经不在播放,无需操作 - if (!isPlaying && !isEngineStarted) { + if (!isEngineStarted) { return true } try { val ret = engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") - isPlaying = false isEngineStarted = false isSynthesisInProgress = false - // 清空队列 + // 清空队列和状态 pendingTexts.clear() isProcessingQueue = false - isContinuousMode = false synthesisFromPlayer = false + pendingPlayCount = 0 // 清空流缓冲区 - streamBuffer.clear() + streamBuffer.delete(0, streamBuffer.length) // 发送取消事件 - dispatchEvent(TtsEvent(TtsEventType.CANCELED)) + dispatchEvent(TtsEventType.SYNTHESIS_CANCELED) return ret == SpeechEngineDefines.ERR_NO_ERROR } catch (e: Exception) { @@ -645,7 +502,7 @@ class VolcanoTtsHelper(private val context: Context) { /** * 释放资源 */ - fun dispose() { + override fun release() { if (!isInitialized) { return } @@ -660,8 +517,8 @@ class VolcanoTtsHelper(private val context: Context) { engine = null isInitialized = false pendingTexts.clear() - streamBuffer.clear() - listeners.clear() + streamBuffer.delete(0, streamBuffer.length) + eventListeners.clear() audioDataListeners.clear() } catch (e: Exception) { Log.e(TAG, "释放引擎异常", e) @@ -678,17 +535,17 @@ class VolcanoTtsHelper(private val context: Context) { currentReqId = stringData isEngineStarted = true - // 如果是连续模式且队列不为空,在引擎启动后立即触发第一句合成 - if (isContinuousMode && !pendingTexts.isEmpty() && !isSynthesisInProgress) { + // 如果队列不为空,在引擎启动后立即触发第一句合成 + if (!pendingTexts.isEmpty() && !isSynthesisInProgress) { triggerNextSynthesis() } } SpeechEngineDefines.MESSAGE_TYPE_ENGINE_STOP -> { // 引擎停止 - isPlaying = false isEngineStarted = false isSynthesisInProgress = false + pendingPlayCount = 0 } SpeechEngineDefines.MESSAGE_TYPE_TTS_SYNTHESIS_BEGIN -> { @@ -703,8 +560,8 @@ class VolcanoTtsHelper(private val context: Context) { // 重置重试计数 retryCount = MAX_RETRY_COUNT - // 如果是连续模式且不是从播放器触发的合成,尝试处理下一个文本 - if (isContinuousMode && !synthesisFromPlayer && !pendingTexts.isEmpty()) { + // 如果不是从播放器触发的合成,尝试处理下一个文本 + if (!synthesisFromPlayer && !pendingTexts.isEmpty()) { // 在合成结束回调中触发下一句合成 triggerNextSynthesis() } @@ -712,28 +569,33 @@ class VolcanoTtsHelper(private val context: Context) { SpeechEngineDefines.MESSAGE_TYPE_TTS_START_PLAYING -> { // 开始播放 - isPlaying = true - dispatchEvent(TtsEvent(TtsEventType.STARTED)) + // Log.d(TAG, "开始播放MESSAGE_TYPE_TTS_START") + dispatchEvent(TtsEventType.SYNTHESIS_STARTED) } SpeechEngineDefines.MESSAGE_TYPE_TTS_FINISH_PLAYING -> { // 播放结束 - isPlaying = false + // Log.d(TAG, "结束播放MESSAGE_TYPE_TTS_FINISH, pendingTexts.size: ${pendingTexts.size}, pendingPlayCount: ${pendingPlayCount}") + + // 减少待播放计数 + if (pendingPlayCount > 0) { + pendingPlayCount-- + } // 如果是从播放器触发的合成,现在可以继续处理队列了 if (synthesisFromPlayer) { synthesisFromPlayer = false // 播放器空闲,尝试触发下一句合成 triggerNextSynthesis() - } else if (pendingTexts.isEmpty()) { - // 如果队列为空,发送播放完成事件并关闭引擎 - dispatchEvent(TtsEvent(TtsEventType.COMPLETED)) + } else if (pendingTexts.isEmpty() && pendingPlayCount == 0) { + // 如果队列为空,发送播放完成事件 + // Log.d(TAG, "所有文本播放完成") + dispatchEvent(TtsEventType.SYNTHESIS_COMPLETED) isProcessingQueue = false - // 如果不是连续模式,关闭引擎 - if (!isContinuousMode) { - closeEngine() - } + // 如果没有待播放内容,关闭引擎 + engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_STOP_ENGINE, "") + isEngineStarted = false } else if (!isSynthesisInProgress) { // 如果队列不为空且当前没有正在进行的合成,触发下一句合成 triggerNextSynthesis() @@ -754,13 +616,21 @@ class VolcanoTtsHelper(private val context: Context) { val errCode = json.optInt("err_code", -1) val errMsg = json.optString("err_msg", "未知错误") - Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg") + Log.e(TAG, "引擎错误: code=$errCode, msg=$errMsg, ${SpeechEngineDefines.CODE_TTS_INVALID_TEXT}") // 标记当前合成结束 isSynthesisInProgress = false // 通知错误 - dispatchEvent(TtsEvent(TtsEventType.ERROR, errCode, errMsg)) + // dispatchEvent(TtsEventType.ERROR, mapOf( + // "errorCode" to errCode.toString(), + // "errorMessage" to errMsg + // )) + + // 检查是否需要更新计数 + if (pendingPlayCount > 0) { + pendingPlayCount-- + } // 尝试恢复或停止 when (errCode) { @@ -774,11 +644,13 @@ class VolcanoTtsHelper(private val context: Context) { SpeechEngineDefines.CODE_TTS_SYNTHESIS_ERROR, SpeechEngineDefines.CODE_TTS_SYNTHESIS_WAITING_TIMEOUT, SpeechEngineDefines.CODE_TTS_ERROR_UNKNOWN -> { - if (isContinuousMode && !pendingTexts.isEmpty()) { + if (!pendingTexts.isEmpty()) { // 轻微错误,直接尝试下一句 + // Log.d(TAG, "轻微错误,直接尝试下一句, ${SpeechEngineDefines.CODE_TTS_INVALID_TEXT}") triggerNextSynthesis() } else { - stop() + + // stop() } } @@ -791,15 +663,10 @@ class VolcanoTtsHelper(private val context: Context) { // 1秒后重试当前文本 mainHandler.postDelayed({ if (isEngineStarted) { - if (isContinuousMode) { - // 设置当前文本并触发合成 - engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, currentText) - isSynthesisInProgress = true - engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "") - } else { - // 非连续模式,重新启动引擎 - startEngineWithText(currentText, false) - } + // 设置当前文本并触发合成 + engine?.setOptionString(engineHandler, SpeechEngineDefines.PARAMS_KEY_TTS_TEXT_STRING, currentText) + isSynthesisInProgress = true + engine?.sendDirective(engineHandler, SpeechEngineDefines.DIRECTIVE_SYNTHESIS, "") } }, 1000) } else { @@ -809,7 +676,8 @@ class VolcanoTtsHelper(private val context: Context) { // 其他错误,停止引擎 else -> { - stop() + Log.d(TAG, "其他错误,停止合成") + // stop() } } } catch (e: Exception) { diff --git a/pubspec.yaml b/pubspec.yaml index ad9f82590..3fe578180 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -65,16 +65,14 @@ dependencies: dio: ^5.8.0+1 sqflite: ^2.4.2 path: ^1.9.1 - azure_speech: - path: local_plugins/azure_speech +# azure_speech: +# path: local_plugins/azure_speech open_ai_service: path: local_plugins/open_ai_service chat_storage: path: local_plugins/chat_storage spotify_sdk: path: local_plugins/spotify_sdk - amap_service: - path: local_plugins/amap_service jl_opus: path: local_plugins/jl_opus jl_ota: @@ -87,10 +85,6 @@ dependencies: path: local_plugins/agent_service music_service: path: local_plugins/music_service - volcano_speech: - path: local_plugins/volcano_speech - bytedance_speech: - path: local_plugins/bytedance_speech dev_dependencies: